Swiftgram/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift
2022-03-18 22:55:06 +04:00

422 lines
25 KiB
Swift

import Foundation
import UIKit
import Postbox
import TelegramCore
import SwiftSignalKit
import Display
import AccountContext
import ChatInterfaceState
func preloadedChatHistoryViewForLocation(_ location: ChatHistoryLocationInput, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, fixedCombinedReadStates: MessageHistoryViewReadState?, tagMask: MessageTags?, additionalData: [AdditionalMessageHistoryViewData], orderStatistics: MessageHistoryViewOrderStatistics = []) -> Signal<ChatHistoryViewUpdate, NoError> {
var isScheduled = false
if case .scheduledMessages = subject {
isScheduled = true
}
var tagMask = tagMask
if case .pinnedMessages = subject {
tagMask = .pinned
}
return (chatHistoryViewForLocation(location, ignoreMessagesInTimestampRange: nil, context: context, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, scheduled: isScheduled, fixedCombinedReadStates: fixedCombinedReadStates, tagMask: tagMask, appendMessagesFromTheSameGroup: false, additionalData: additionalData, orderStatistics: orderStatistics)
|> castError(Bool.self)
|> mapToSignal { update -> Signal<ChatHistoryViewUpdate, Bool> in
switch update {
case let .Loading(_, type):
if case .Generic(.FillHole) = type {
return .fail(true)
}
case let .HistoryView(_, type, _, _, _, _, _):
if case .Generic(.FillHole) = type {
return .fail(true)
}
}
return .single(update)
})
|> restartIfError
}
func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMessagesInTimestampRange: ClosedRange<Int32>?, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, scheduled: Bool, fixedCombinedReadStates: MessageHistoryViewReadState?, tagMask: MessageTags?, appendMessagesFromTheSameGroup: Bool, additionalData: [AdditionalMessageHistoryViewData], orderStatistics: MessageHistoryViewOrderStatistics = []) -> Signal<ChatHistoryViewUpdate, NoError> {
let account = context.account
if scheduled {
var first = true
var chatScrollPosition: ChatHistoryViewScrollPosition?
if case let .Scroll(index, _, sourceIndex, position, animated, highlight) = location.content {
let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up
chatScrollPosition = .index(index: index, position: position, directionHint: directionHint, animated: animated, highlight: highlight)
}
return account.viewTracker.scheduledMessagesViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), additionalData: additionalData)
|> map { view, updateType, initialData -> ChatHistoryViewUpdate in
let (cachedData, cachedDataMessages, readStateData) = extractAdditionalData(view: view, chatLocation: chatLocation)
let combinedInitialData = ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData)
if view.isLoading {
return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType))
}
let type: ChatHistoryViewUpdateType
let scrollPosition: ChatHistoryViewScrollPosition? = first ? chatScrollPosition : nil
if first {
first = false
if chatScrollPosition == nil {
type = .Initial(fadeIn: false)
} else {
type = .Generic(type: .UpdateVisible)
}
} else {
type = .Generic(type: .Generic)
}
return .HistoryView(view: view, type: type, scrollPosition: scrollPosition, flashIndicators: false, originalScrollPosition: chatScrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id)
}
} else {
let ignoreRelatedChats: Bool
if let tagMask = tagMask, case .pinned = tagMask {
ignoreRelatedChats = true
} else {
ignoreRelatedChats = false
}
switch location.content {
case let .Initial(count):
var preloaded = false
var fadeIn = false
let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError>
if let tagMask = tagMask {
signal = account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, index: .upperBound, anchorIndex: .upperBound, count: count, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: nil, tagMask: tagMask, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics)
} else {
signal = account.viewTracker.aroundMessageOfInterestHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, tagMask: tagMask, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData)
}
return signal
|> map { view, updateType, initialData -> ChatHistoryViewUpdate in
let (cachedData, cachedDataMessages, readStateData) = extractAdditionalData(view: view, chatLocation: chatLocation)
let combinedInitialData = ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData)
if preloaded {
return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, flashIndicators: false, originalScrollPosition: nil, initialData: combinedInitialData, id: location.id)
} else {
if view.isLoading {
return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType))
}
var scrollPosition: ChatHistoryViewScrollPosition?
let canScrollToRead: Bool
if case .replyThread = chatLocation {
canScrollToRead = true
} else if view.isAddedToChatList {
canScrollToRead = true
} else {
canScrollToRead = false
}
if let maxReadIndex = view.maxReadIndex, tagMask == nil, canScrollToRead {
let aroundIndex = maxReadIndex
scrollPosition = .unread(index: maxReadIndex)
if case .peer = chatLocation {
var targetIndex = 0
for i in 0 ..< view.entries.count {
if view.entries[i].index >= aroundIndex {
targetIndex = i
break
}
}
let maxIndex = targetIndex + count / 2
let minIndex = targetIndex - count / 2
if minIndex <= 0 && view.holeEarlier {
fadeIn = true
return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType))
}
if maxIndex >= targetIndex {
if view.holeLater {
fadeIn = true
return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType))
}
if view.holeEarlier {
var incomingCount: Int32 = 0
inner: for entry in view.entries.reversed() {
if !entry.message.flags.intersection(.IsIncomingMask).isEmpty {
incomingCount += 1
}
}
if case let .peer(peerId) = chatLocation, let combinedReadStates = view.fixedReadStates, case let .peer(readStates) = combinedReadStates, let readState = readStates[peerId], readState.count == incomingCount {
} else {
fadeIn = true
return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType))
}
}
}
}
} else if view.isAddedToChatList, tagMask == nil, let historyScrollState = (initialData?.storedInterfaceState).flatMap(_internal_decodeStoredChatInterfaceState).flatMap(ChatInterfaceState.parse)?.historyScrollState {
scrollPosition = .positionRestoration(index: historyScrollState.messageIndex, relativeOffset: CGFloat(historyScrollState.relativeOffset))
} else {
if case .peer = chatLocation, !view.isAddedToChatList {
if view.holeEarlier && view.entries.count <= 2 {
fadeIn = true
return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType))
}
}
if view.entries.isEmpty && (view.holeEarlier || view.holeLater) {
fadeIn = true
return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType))
}
}
preloaded = true
return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id)
}
}
case let .InitialSearch(searchLocation, count, highlight):
var preloaded = false
var fadeIn = false
let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError>
switch searchLocation {
case let .index(index):
signal = account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, index: .message(index), anchorIndex: .message(index), count: count, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: nil, tagMask: tagMask, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData)
case let .id(id):
signal = account.viewTracker.aroundIdMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, ignoreRelatedChats: ignoreRelatedChats, messageId: id, tagMask: tagMask, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData)
}
return signal |> map { view, updateType, initialData -> ChatHistoryViewUpdate in
let (cachedData, cachedDataMessages, readStateData) = extractAdditionalData(view: view, chatLocation: chatLocation)
let combinedInitialData = ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData)
if preloaded {
return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, flashIndicators: false, originalScrollPosition: nil, initialData: combinedInitialData, id: location.id)
} else {
let anchorIndex = view.anchorIndex
var targetIndex = 0
for i in 0 ..< view.entries.count {
if anchorIndex.isLessOrEqual(to: view.entries[i].index) {
targetIndex = i
break
}
}
if !view.entries.isEmpty {
let minIndex = max(0, targetIndex - count / 2)
let maxIndex = min(view.entries.count, targetIndex + count / 2)
if minIndex == 0 && view.holeEarlier {
fadeIn = true
return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType))
}
if maxIndex == view.entries.count && view.holeLater {
fadeIn = true
return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType))
}
} else if view.holeEarlier || view.holeLater {
fadeIn = true
return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType))
}
var reportUpdateType: ChatHistoryViewUpdateType = .Initial(fadeIn: fadeIn)
if case .FillHole = updateType {
reportUpdateType = .Generic(type: updateType)
}
preloaded = true
return .HistoryView(view: view, type: reportUpdateType, scrollPosition: .index(index: anchorIndex, position: .center(.bottom), directionHint: .Down, animated: false, highlight: highlight), flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id)
}
}
case let .Navigation(index, anchorIndex, count, _):
var first = true
return account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, index: index, anchorIndex: anchorIndex, count: count, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates, tagMask: tagMask, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in
let (cachedData, cachedDataMessages, readStateData) = extractAdditionalData(view: view, chatLocation: chatLocation)
let genericType: ViewUpdateType
if first {
first = false
genericType = ViewUpdateType.UpdateVisible
} else {
genericType = updateType
}
return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id)
}
case let .Scroll(index, anchorIndex, sourceIndex, scrollPosition, animated, highlight):
let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up
let chatScrollPosition = ChatHistoryViewScrollPosition.index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated, highlight: highlight)
var first = true
return account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, index: index, anchorIndex: anchorIndex, count: 128, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates, tagMask: tagMask, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData)
|> map { view, updateType, initialData -> ChatHistoryViewUpdate in
let (cachedData, cachedDataMessages, readStateData) = extractAdditionalData(view: view, chatLocation: chatLocation)
let combinedInitialData = ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData)
if view.isLoading {
return ChatHistoryViewUpdate.Loading(initialData: combinedInitialData, type: .Generic(type: updateType))
}
let genericType: ViewUpdateType
let scrollPosition: ChatHistoryViewScrollPosition? = first ? chatScrollPosition : nil
if first {
first = false
genericType = ViewUpdateType.UpdateVisible
} else {
genericType = updateType
}
return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: scrollPosition, flashIndicators: animated, originalScrollPosition: chatScrollPosition, initialData: combinedInitialData, id: location.id)
}
}
}
}
private func extractAdditionalData(view: MessageHistoryView, chatLocation: ChatLocation) -> (
cachedData: CachedPeerData?,
cachedDataMessages: [MessageId: Message]?,
readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]?
) {
var cachedData: CachedPeerData?
var cachedDataMessages: [MessageId: Message] = [:]
var readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData] = [:]
var notificationSettings: PeerNotificationSettings?
loop: for data in view.additionalData {
switch data {
case let .peerNotificationSettings(value):
notificationSettings = value
default:
break
}
}
for data in view.additionalData {
switch data {
case let .peerNotificationSettings(value):
notificationSettings = value
case let .cachedPeerData(peerIdValue, value):
if chatLocation.peerId == peerIdValue {
cachedData = value
}
case let .cachedPeerDataMessages(peerIdValue, value):
if case .peer(peerIdValue) = chatLocation {
if let value = value {
for (_, message) in value {
cachedDataMessages[message.id] = message
}
}
}
case let .message(_, messages):
for message in messages {
cachedDataMessages[message.id] = message
}
case let .totalUnreadState(totalUnreadState):
switch chatLocation {
case let .peer(peerId):
if let combinedReadStates = view.fixedReadStates {
if case let .peer(readStates) = combinedReadStates, let readState = readStates[peerId] {
readStateData[peerId] = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalState: totalUnreadState, notificationSettings: notificationSettings)
}
}
case .replyThread, .feed:
break
}
default:
break
}
}
return (cachedData, cachedDataMessages, readStateData)
}
struct ReplyThreadInfo {
var message: ChatReplyThreadMessage
var isChannelPost: Bool
var isEmpty: Bool
var scrollToLowerBoundMessage: MessageIndex?
var contextHolder: Atomic<ChatLocationContextHolder?>
}
enum ReplyThreadSubject {
case channelPost(MessageId)
case groupMessage(MessageId)
}
func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThreadSubject, atMessageId: MessageId?) -> Signal<ReplyThreadInfo, FetchChannelReplyThreadMessageError> {
let message: Signal<ChatReplyThreadMessage, FetchChannelReplyThreadMessageError>
switch subject {
case .channelPost(let messageId), .groupMessage(let messageId):
message = context.engine.messages.fetchChannelReplyThreadMessage(messageId: messageId, atMessageId: atMessageId)
}
return message
|> mapToSignal { replyThreadMessage -> Signal<ReplyThreadInfo, FetchChannelReplyThreadMessageError> in
let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
let input: ChatHistoryLocationInput
var scrollToLowerBoundMessage: MessageIndex?
switch replyThreadMessage.initialAnchor {
case .automatic:
if let atMessageId = atMessageId {
input = ChatHistoryLocationInput(
content: .InitialSearch(location: .id(atMessageId), count: 40, highlight: true),
id: 0
)
} else {
input = ChatHistoryLocationInput(
content: .Initial(count: 40),
id: 0
)
}
case let .lowerBoundMessage(index):
input = ChatHistoryLocationInput(
content: .Navigation(index: .message(index), anchorIndex: .message(index), count: 40, highlight: false),
id: 0
)
scrollToLowerBoundMessage = index
}
if replyThreadMessage.isNotAvailable {
return .single(ReplyThreadInfo(
message: replyThreadMessage,
isChannelPost: replyThreadMessage.isChannelPost,
isEmpty: false,
scrollToLowerBoundMessage: nil,
contextHolder: chatLocationContextHolder
))
}
let preloadSignal = preloadedChatHistoryViewForLocation(
input,
context: context,
chatLocation: .replyThread(message: replyThreadMessage),
subject: nil,
chatLocationContextHolder: chatLocationContextHolder,
fixedCombinedReadStates: nil,
tagMask: nil,
additionalData: []
)
return preloadSignal
|> map { historyView -> Bool? in
switch historyView {
case .Loading:
return nil
case let .HistoryView(view, _, _, _, _, _, _):
return view.entries.isEmpty
}
}
|> mapToSignal { value -> Signal<Bool, NoError> in
if let value = value {
return .single(value)
} else {
return .complete()
}
}
|> take(1)
|> map { isEmpty -> ReplyThreadInfo in
return ReplyThreadInfo(
message: replyThreadMessage,
isChannelPost: replyThreadMessage.isChannelPost,
isEmpty: isEmpty,
scrollToLowerBoundMessage: scrollToLowerBoundMessage,
contextHolder: chatLocationContextHolder
)
}
|> castError(FetchChannelReplyThreadMessageError.self)
}
}