mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-04-07 13:47:54 +00:00
Fixes
fix localeWithStrings globally (#30)
Fix badge on zoomed devices. closes #9
Hide channel bottom panel closes #27
Another attempt to fix badge on some Zoomed devices
Force System Share sheet tg://sg/debug
fixes for device badge
New Crowdin updates (#34)
* New translations sglocalizable.strings (Chinese Traditional)
* New translations sglocalizable.strings (Chinese Simplified)
* New translations sglocalizable.strings (Chinese Traditional)
Fix input panel hidden on selection (#31)
* added if check for selectionState != nil
* same order of subnodes
Revert "Fix input panel hidden on selection (#31)"
This reverts commit e8a8bb1496.
Fix input panel for channels Closes #37
Quickly share links with system's share menu
force tabbar when editing
increase height for correct animation
New translations sglocalizable.strings (Ukrainian) (#38)
Hide Post Story button
Fix 10.15.1
Fix archive option for long-tap
Enable in-app Safari
Disable some unsupported purchases
disableDeleteChatSwipeOption + refactor restart alert
Hide bot in suggestions list
Fix merge v11.0
Fix exceptions for safari webview controller
New Crowdin updates (#47)
* New translations sglocalizable.strings (Romanian)
* New translations sglocalizable.strings (French)
* New translations sglocalizable.strings (Spanish)
* New translations sglocalizable.strings (Afrikaans)
* New translations sglocalizable.strings (Arabic)
* New translations sglocalizable.strings (Catalan)
* New translations sglocalizable.strings (Czech)
* New translations sglocalizable.strings (Danish)
* New translations sglocalizable.strings (German)
* New translations sglocalizable.strings (Greek)
* New translations sglocalizable.strings (Finnish)
* New translations sglocalizable.strings (Hebrew)
* New translations sglocalizable.strings (Hungarian)
* New translations sglocalizable.strings (Italian)
* New translations sglocalizable.strings (Japanese)
* New translations sglocalizable.strings (Korean)
* New translations sglocalizable.strings (Dutch)
* New translations sglocalizable.strings (Norwegian)
* New translations sglocalizable.strings (Polish)
* New translations sglocalizable.strings (Portuguese)
* New translations sglocalizable.strings (Serbian (Cyrillic))
* New translations sglocalizable.strings (Swedish)
* New translations sglocalizable.strings (Turkish)
* New translations sglocalizable.strings (Vietnamese)
* New translations sglocalizable.strings (Indonesian)
* New translations sglocalizable.strings (Hindi)
* New translations sglocalizable.strings (Uzbek)
New Crowdin updates (#49)
* New translations sglocalizable.strings (Arabic)
* New translations sglocalizable.strings (Arabic)
New translations sglocalizable.strings (Russian) (#51)
Call confirmation
WIP Settings search
Settings Search
Localize placeholder
Update AccountUtils.swift
mark mutual contact
Align back context action to left
New Crowdin updates (#54)
* New translations sglocalizable.strings (Chinese Simplified)
* New translations sglocalizable.strings (Chinese Traditional)
* New translations sglocalizable.strings (Ukrainian)
Independent Playground app for simulator
New translations sglocalizable.strings (Ukrainian) (#55)
Playground UIKit base and controllers
Inject SwiftUI view with overflow to AsyncDisplayKit
Launch Playgound project on simulator
Create .swiftformat
Move Playground to example
Update .swiftformat
Init SwiftUIViewController
wip
New translations sglocalizable.strings (Chinese Traditional) (#57)
Xcode 16 fixes
Fix
New translations sglocalizable.strings (Italian) (#59)
New translations sglocalizable.strings (Chinese Simplified) (#63)
Force disable CallKit integration due to missing NSE Entitlement
Fix merge
Fix whole chat translator
Sweetpad config
Bump version
11.3.1 fixes
Mutual contact placement fix
Disable Video PIP swipe
Update versions.json
Fix PIP crash
4794 lines
261 KiB
Swift
4794 lines
261 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 ChatMessageItem
|
|
import ChatMessageItemImpl
|
|
import ChatMessageItemView
|
|
import ChatMessageBubbleItemNode
|
|
import ChatMessageTransitionNode
|
|
import ChatControllerInteraction
|
|
import DustEffect
|
|
import UrlHandling
|
|
|
|
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)
|
|
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(title, text, photo, video, presentationData):
|
|
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatBotInfoItem(title: title, text: text, photo: photo, video: video, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context), 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)
|
|
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(title, text, photo, video, presentationData):
|
|
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatBotInfoItem(title: title, text: text, photo: photo, video: video, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context), 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)
|
|
|
|
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()
|
|
}
|
|
|
|
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?) {
|
|
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 {
|
|
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.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)))
|
|
}
|
|
if [Namespaces.Peer.CloudChannel, Namespaces.Peer.CloudGroup].contains(peerId.namespace) {
|
|
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)
|
|
} 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 messageViewQueue = Queue.mainQueue()
|
|
let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue,
|
|
self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) |> take(1),
|
|
historyViewUpdate,
|
|
self.chatPresentationDataPromise.get(),
|
|
selectedMessages,
|
|
updatingMedia,
|
|
automaticDownloadNetworkType,
|
|
preferredStoryHighQuality,
|
|
animatedEmojiStickers,
|
|
additionalAnimatedEmojiStickers,
|
|
customChannelDiscussionReadState,
|
|
customThreadOutgoingReadState,
|
|
availableReactions,
|
|
availableMessageEffects,
|
|
savedMessageTags,
|
|
defaultReaction,
|
|
accountPeer,
|
|
audioTranscriptionSuggestion,
|
|
promises,
|
|
topicAuthorId,
|
|
translationState,
|
|
maxReadStoryId,
|
|
recommendedChannels,
|
|
audioTranscriptionTrial,
|
|
chatThemes,
|
|
deviceContactsNumbers,
|
|
contentSettings
|
|
).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
|
|
|
|
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 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, let boostLevel = channel.approximateBoostLevel {
|
|
if boostLevel >= premiumConfiguration.minGroupAudioTranscriptionLevel {
|
|
audioTranscriptionProvidedByBoost = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let alwaysDisplayTranscribeButton = ChatMessageItemAssociatedData.DisplayTranscribeButton(
|
|
canBeDisplayed: suggestAudioTranscription.0 < 2,
|
|
displayForNotConsumed: suggestAudioTranscription.1,
|
|
providedByGroupBoost: audioTranscriptionProvidedByBoost
|
|
)
|
|
// MARK: Swiftgram
|
|
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: String?
|
|
if let translationState, (isPremium || true) && translationState.isEnabled {
|
|
translateToLanguage = 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, 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: nil, toLang: translateToLanguage)
|
|
} 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,
|
|
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: (String, [(Int, NSRange)])
|
|
let releaseText: (String, [(Int, NSRange)])
|
|
switch nextChannelToRead.location {
|
|
case .same:
|
|
if let controllerNode = self.controllerInteraction.chatControllerNode() as? ChatControllerNode, let chatController = controllerNode.interfaceInteraction?.chatController() as? ChatControllerImpl, chatController.customChatNavigationStack != nil {
|
|
swipeText = (self.currentPresentationData.strings.Chat_NextSuggestedChannelSwipeProgress, [])
|
|
releaseText = (self.currentPresentationData.strings.Chat_NextSuggestedChannelSwipeAction, [])
|
|
} else if nextChannelToRead.threadData != nil {
|
|
swipeText = (self.currentPresentationData.strings.Chat_NextUnreadTopicSwipeProgress, [])
|
|
releaseText = (self.currentPresentationData.strings.Chat_NextUnreadTopicSwipeAction, [])
|
|
} else {
|
|
swipeText = (self.currentPresentationData.strings.Chat_NextChannelSameLocationSwipeProgress, [])
|
|
releaseText = (self.currentPresentationData.strings.Chat_NextChannelSameLocationSwipeAction, [])
|
|
}
|
|
case .archived:
|
|
swipeText = (self.currentPresentationData.strings.Chat_NextChannelArchivedSwipeProgress, [])
|
|
releaseText = (self.currentPresentationData.strings.Chat_NextChannelArchivedSwipeAction, [])
|
|
case .unarchived:
|
|
swipeText = (self.currentPresentationData.strings.Chat_NextChannelUnarchivedSwipeProgress, [])
|
|
releaseText = (self.currentPresentationData.strings.Chat_NextChannelUnarchivedSwipeAction, [])
|
|
case let .folder(_, title):
|
|
swipeText = self.currentPresentationData.strings.Chat_NextChannelFolderSwipeProgress(title)._tuple
|
|
releaseText = self.currentPresentationData.strings.Chat_NextChannelFolderSwipeAction(title)._tuple
|
|
}
|
|
|
|
if expandProgress < 0.1 {
|
|
chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: nil)
|
|
} else if expandProgress >= 1.0 {
|
|
if chatControllerNode.inputPanelOverscrollNode?.text.0 != releaseText.0 {
|
|
chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: ChatInputPanelOverscrollNode(text: releaseText, color: self.currentPresentationData.theme.theme.rootController.navigationBar.secondaryTextColor, priority: 1))
|
|
}
|
|
} else {
|
|
if chatControllerNode.inputPanelOverscrollNode?.text.0 != swipeText.0 {
|
|
chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: ChatInputPanelOverscrollNode(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 {
|
|
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, _, _, _, _, _):
|
|
var hasAction = false
|
|
for media in message.media {
|
|
if let _ = media as? TelegramMediaAction {
|
|
hasAction = true
|
|
}
|
|
}
|
|
if let _ = message.inlineBotAttribute {
|
|
if let visibleBusinessBotMessageIdValue = visibleBusinessBotMessageId {
|
|
if visibleBusinessBotMessageIdValue < message.id {
|
|
visibleBusinessBotMessageId = message.id
|
|
}
|
|
} else {
|
|
visibleBusinessBotMessageId = message.id
|
|
}
|
|
}
|
|
if !hasAction {
|
|
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 {
|
|
var hasAction = false
|
|
for media in message.media {
|
|
if let _ = media as? TelegramMediaAction {
|
|
hasAction = true
|
|
}
|
|
}
|
|
if let _ = message.inlineBotAttribute {
|
|
if let visibleBusinessBotMessageIdValue = visibleBusinessBotMessageId {
|
|
if visibleBusinessBotMessageIdValue < message.id {
|
|
visibleBusinessBotMessageId = message.id
|
|
}
|
|
} else {
|
|
visibleBusinessBotMessageId = message.id
|
|
}
|
|
}
|
|
if !hasAction {
|
|
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 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 {
|
|
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
|
|
}
|
|
}
|