import Foundation import SyncCore import Postbox import SwiftSignalKit import TelegramApi private struct DiscussionMessage { public var messageId: MessageId public var channelMessageId: MessageId? public var isChannelPost: Bool public var maxMessage: MessageId? public var maxReadIncomingMessageId: MessageId? public var maxReadOutgoingMessageId: MessageId? } private class ReplyThreadHistoryContextImpl { private let queue: Queue private let account: Account private let messageId: MessageId private var currentHole: (MessageHistoryHolesViewEntry, Disposable)? struct State: Equatable { var messageId: MessageId var holeIndices: [MessageId.Namespace: IndexSet] var maxReadIncomingMessageId: MessageId? var maxReadOutgoingMessageId: MessageId? } let state = Promise() private var stateValue: State? { didSet { if let stateValue = self.stateValue { if stateValue != oldValue { self.state.set(.single(stateValue)) } } } } let maxReadOutgoingMessageId = Promise() private var maxReadOutgoingMessageIdValue: MessageId? { didSet { if self.maxReadOutgoingMessageIdValue != oldValue { self.maxReadOutgoingMessageId.set(.single(self.maxReadOutgoingMessageIdValue)) } } } private var initialStateDisposable: Disposable? private var holesDisposable: Disposable? private var readStateDisposable: Disposable? private var updateInitialStateDisposable: Disposable? private let readDisposable = MetaDisposable() init(queue: Queue, account: Account, data: ChatReplyThreadMessage) { self.queue = queue self.account = account self.messageId = data.messageId self.maxReadOutgoingMessageIdValue = data.maxReadOutgoingMessageId self.maxReadOutgoingMessageId.set(.single(self.maxReadOutgoingMessageIdValue)) self.initialStateDisposable = (account.postbox.transaction { transaction -> State in var indices = transaction.getThreadIndexHoles(peerId: data.messageId.peerId, threadId: makeMessageThreadId(data.messageId), namespace: Namespaces.Message.Cloud) indices.subtract(data.initialFilledHoles) let isParticipant = transaction.getPeerChatListIndex(data.messageId.peerId) != nil if isParticipant { let historyHoles = transaction.getHoles(peerId: data.messageId.peerId, namespace: Namespaces.Message.Cloud) indices.formIntersection(historyHoles) } if let maxMessageId = data.maxMessage { indices.remove(integersIn: Int(maxMessageId.id + 1) ..< Int(Int32.max)) } else { indices.removeAll() } return State(messageId: data.messageId, holeIndices: [Namespaces.Message.Cloud: indices], maxReadIncomingMessageId: data.maxReadIncomingMessageId, maxReadOutgoingMessageId: data.maxReadOutgoingMessageId) } |> deliverOn(self.queue)).start(next: { [weak self] state in guard let strongSelf = self else { return } strongSelf.stateValue = state strongSelf.state.set(.single(state)) }) let threadId = makeMessageThreadId(messageId) self.holesDisposable = (account.postbox.messageHistoryHolesView() |> map { view -> MessageHistoryHolesViewEntry? in for entry in view.entries { switch entry.hole { case let .peer(hole): if hole.threadId == threadId { return entry } } } return nil } |> distinctUntilChanged |> deliverOn(self.queue)).start(next: { [weak self] entry in guard let strongSelf = self else { return } strongSelf.setCurrentHole(entry: entry) }) self.readStateDisposable = (account.stateManager.threadReadStateUpdates |> deliverOn(self.queue)).start(next: { [weak self] (_, outgoing) in guard let strongSelf = self else { return } if let value = outgoing[data.messageId] { strongSelf.maxReadOutgoingMessageIdValue = MessageId(peerId: data.messageId.peerId, namespace: Namespaces.Message.Cloud, id: value) } }) let updateInitialState: Signal = account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(data.messageId.peerId).flatMap(apiInputPeer) } |> castError(FetchChannelReplyThreadMessageError.self) |> mapToSignal { inputPeer -> Signal in guard let inputPeer = inputPeer else { return .fail(.generic) } return account.network.request(Api.functions.messages.getDiscussionMessage(peer: inputPeer, msgId: data.messageId.id)) |> mapError { _ -> FetchChannelReplyThreadMessageError in return .generic } |> mapToSignal { discussionMessage -> Signal in return account.postbox.transaction { transaction -> Signal in switch discussionMessage { case let .discussionMessage(_, messages, maxId, readInboxMaxId, readOutboxMaxId, chats, users): let parsedMessages = messages.compactMap { message -> StoreMessage? in StoreMessage(apiMessage: message) } guard let topMessage = parsedMessages.last, let parsedIndex = topMessage.index else { return .fail(.generic) } var channelMessageId: MessageId? var replyThreadAttribute: ReplyThreadMessageAttribute? for attribute in topMessage.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { channelMessageId = attribute.messageId } else if let attribute = attribute as? ReplyThreadMessageAttribute { replyThreadAttribute = attribute } } var peers: [Peer] = [] var peerPresences: [PeerId: PeerPresence] = [:] for chat in chats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { peers.append(groupOrChannel) } } for user in users { let telegramUser = TelegramUser(user: user) peers.append(telegramUser) if let presence = TelegramUserPresence(apiUser: user) { peerPresences[telegramUser.id] = presence } } let _ = transaction.addMessages(parsedMessages, location: .Random) updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in return updated }) updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences) let resolvedMaxMessage: MessageId? if let maxId = maxId { resolvedMaxMessage = MessageId( peerId: parsedIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: maxId ) } else { resolvedMaxMessage = nil } var isChannelPost = false for attribute in topMessage.attributes { if let _ = attribute as? SourceReferenceMessageAttribute { isChannelPost = true break } } let maxReadIncomingMessageId = readInboxMaxId.flatMap { readMaxId in MessageId(peerId: parsedIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: readMaxId) } if let channelMessageId = channelMessageId, let replyThreadAttribute = replyThreadAttribute { account.viewTracker.updateReplyInfoForMessageId(channelMessageId, info: AccountViewTracker.UpdatedMessageReplyInfo( timestamp: Int32(CFAbsoluteTimeGetCurrent()), commentsPeerId: parsedIndex.id.peerId, maxReadIncomingMessageId: maxReadIncomingMessageId, maxMessageId: resolvedMaxMessage )) transaction.updateMessage(channelMessageId, update: { currentMessage in var attributes = currentMessage.attributes loop: for j in 0 ..< attributes.count { if let attribute = attributes[j] as? ReplyThreadMessageAttribute { attributes[j] = ReplyThreadMessageAttribute( count: replyThreadAttribute.count, latestUsers: attribute.latestUsers, commentsPeerId: attribute.commentsPeerId, maxMessageId: replyThreadAttribute.maxMessageId, maxReadMessageId: replyThreadAttribute.maxReadMessageId ) } } return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) }) } return .single(DiscussionMessage( messageId: parsedIndex.id, channelMessageId: channelMessageId, isChannelPost: isChannelPost, maxMessage: resolvedMaxMessage, maxReadIncomingMessageId: maxReadIncomingMessageId, maxReadOutgoingMessageId: readOutboxMaxId.flatMap { readMaxId in MessageId(peerId: parsedIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: readMaxId) } )) } } |> castError(FetchChannelReplyThreadMessageError.self) |> switchToLatest } } self.updateInitialStateDisposable = (updateInitialState |> deliverOnMainQueue).start(next: { [weak self] updatedData in guard let strongSelf = self else { return } if let maxReadOutgoingMessageId = updatedData.maxReadOutgoingMessageId { if let current = strongSelf.maxReadOutgoingMessageIdValue { if maxReadOutgoingMessageId > current { strongSelf.maxReadOutgoingMessageIdValue = maxReadOutgoingMessageId } } else { strongSelf.maxReadOutgoingMessageIdValue = maxReadOutgoingMessageId } } }) } deinit { self.initialStateDisposable?.dispose() self.holesDisposable?.dispose() self.readDisposable.dispose() self.updateInitialStateDisposable?.dispose() } func setCurrentHole(entry: MessageHistoryHolesViewEntry?) { if self.currentHole?.0 != entry { self.currentHole?.1.dispose() if let entry = entry { self.currentHole = (entry, self.fetchHole(entry: entry).start(next: { [weak self] removedHoleIndices in guard let strongSelf = self, let removedHoleIndices = removedHoleIndices else { return } if var currentHoles = strongSelf.stateValue?.holeIndices[Namespaces.Message.Cloud] { currentHoles.subtract(removedHoleIndices.removedIndices) strongSelf.stateValue?.holeIndices[Namespaces.Message.Cloud] = currentHoles } })) } else { self.currentHole = nil } } } private func fetchHole(entry: MessageHistoryHolesViewEntry) -> Signal { switch entry.hole { case let .peer(hole): let fetchCount = min(entry.count, 100) return fetchMessageHistoryHole(accountPeerId: self.account.peerId, source: .network(self.account.network), postbox: self.account.postbox, peerInput: .direct(peerId: hole.peerId, threadId: hole.threadId), namespace: hole.namespace, direction: entry.direction, space: entry.space, count: fetchCount) } } func applyMaxReadIndex(messageIndex: MessageIndex) { let account = self.account let messageId = self.messageId if messageIndex.id.namespace != messageId.namespace { return } let signal = self.account.postbox.transaction { transaction -> Api.InputPeer? in if let message = transaction.getMessage(messageId) { for attribute in message.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { if let sourceMessage = transaction.getMessage(attribute.messageId) { account.viewTracker.applyMaxReadIncomingMessageIdForReplyInfo(id: attribute.messageId, maxReadIncomingMessageId: messageIndex.id) var updatedAttribute: ReplyThreadMessageAttribute? for i in 0 ..< sourceMessage.attributes.count { if let attribute = sourceMessage.attributes[i] as? ReplyThreadMessageAttribute { if let maxReadMessageId = attribute.maxReadMessageId { if maxReadMessageId < messageIndex.id.id { updatedAttribute = ReplyThreadMessageAttribute(count: attribute.count, latestUsers: attribute.latestUsers, commentsPeerId: attribute.commentsPeerId, maxMessageId: attribute.maxMessageId, maxReadMessageId: messageIndex.id.id) } } else { updatedAttribute = ReplyThreadMessageAttribute(count: attribute.count, latestUsers: attribute.latestUsers, commentsPeerId: attribute.commentsPeerId, maxMessageId: attribute.maxMessageId, maxReadMessageId: messageIndex.id.id) } break } } if let updatedAttribute = updatedAttribute { transaction.updateMessage(sourceMessage.id, update: { currentMessage in var attributes = currentMessage.attributes loop: for j in 0 ..< attributes.count { if let _ = attributes[j] as? ReplyThreadMessageAttribute { attributes[j] = updatedAttribute } } return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) }) } } break } } } return transaction.getPeer(messageIndex.id.peerId).flatMap(apiInputPeer) } |> mapToSignal { inputPeer -> Signal in guard let inputPeer = inputPeer else { return .complete() } return account.network.request(Api.functions.messages.readDiscussion(peer: inputPeer, msgId: messageId.id, readMaxId: messageIndex.id.id)) |> `catch` { _ -> Signal in return .single(.boolFalse) } |> ignoreValues } self.readDisposable.set(signal.start()) } } public class ReplyThreadHistoryContext { fileprivate final class GuardReference { private let deallocated: () -> Void init(deallocated: @escaping () -> Void) { self.deallocated = deallocated } deinit { self.deallocated() } } private let queue = Queue() private let impl: QueueLocalObject public var state: Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in let stateDisposable = impl.state.get().start(next: { state in subscriber.putNext(MessageHistoryViewExternalInput( peerId: state.messageId.peerId, threadId: makeMessageThreadId(state.messageId), maxReadIncomingMessageId: state.maxReadIncomingMessageId, maxReadOutgoingMessageId: state.maxReadOutgoingMessageId, holes: state.holeIndices )) }) disposable.set(stateDisposable) } return disposable } } public var maxReadOutgoingMessageId: Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in disposable.set(impl.maxReadOutgoingMessageId.get().start(next: { value in subscriber.putNext(value) })) } return disposable } } public init(account: Account, peerId: PeerId, data: ChatReplyThreadMessage) { let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { return ReplyThreadHistoryContextImpl(queue: queue, account: account, data: data) }) } public func applyMaxReadIndex(messageIndex: MessageIndex) { self.impl.with { impl in impl.applyMaxReadIndex(messageIndex: messageIndex) } } } public struct ChatReplyThreadMessage: Equatable { public enum Anchor: Equatable { case automatic case lowerBoundMessage(MessageIndex) } public var messageId: MessageId public var channelMessageId: MessageId? public var isChannelPost: Bool public var maxMessage: MessageId? public var maxReadIncomingMessageId: MessageId? public var maxReadOutgoingMessageId: MessageId? public var initialFilledHoles: IndexSet public var initialAnchor: Anchor public var isNotAvailable: Bool fileprivate init(messageId: MessageId, channelMessageId: MessageId?, isChannelPost: Bool, maxMessage: MessageId?, maxReadIncomingMessageId: MessageId?, maxReadOutgoingMessageId: MessageId?, initialFilledHoles: IndexSet, initialAnchor: Anchor, isNotAvailable: Bool) { self.messageId = messageId self.channelMessageId = channelMessageId self.isChannelPost = isChannelPost self.maxMessage = maxMessage self.maxReadIncomingMessageId = maxReadIncomingMessageId self.maxReadOutgoingMessageId = maxReadOutgoingMessageId self.initialFilledHoles = initialFilledHoles self.initialAnchor = initialAnchor self.isNotAvailable = isNotAvailable } } public enum FetchChannelReplyThreadMessageError { case generic } public func fetchChannelReplyThreadMessage(account: Account, messageId: MessageId, atMessageId: MessageId?) -> Signal { return account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) } |> castError(FetchChannelReplyThreadMessageError.self) |> mapToSignal { inputPeer -> Signal in guard let inputPeer = inputPeer else { return .fail(.generic) } let replyInfo = Promise() replyInfo.set(account.viewTracker.replyInfoForMessageId(messageId)) let remoteDiscussionMessageSignal: Signal = account.network.request(Api.functions.messages.getDiscussionMessage(peer: inputPeer, msgId: messageId.id)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> mapToSignal { discussionMessage -> Signal in guard let discussionMessage = discussionMessage else { return .single(nil) } return account.postbox.transaction { transaction -> DiscussionMessage? in switch discussionMessage { case let .discussionMessage(_, messages, maxId, readInboxMaxId, readOutboxMaxId, chats, users): let parsedMessages = messages.compactMap { message -> StoreMessage? in StoreMessage(apiMessage: message) } guard let topMessage = parsedMessages.last, let parsedIndex = topMessage.index else { return nil } var channelMessageId: MessageId? for attribute in topMessage.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { channelMessageId = attribute.messageId break } } var peers: [Peer] = [] var peerPresences: [PeerId: PeerPresence] = [:] for chat in chats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { peers.append(groupOrChannel) } } for user in users { let telegramUser = TelegramUser(user: user) peers.append(telegramUser) if let presence = TelegramUserPresence(apiUser: user) { peerPresences[telegramUser.id] = presence } } let _ = transaction.addMessages(parsedMessages, location: .Random) updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in return updated }) updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences) let resolvedMaxMessage: MessageId? if let maxId = maxId { resolvedMaxMessage = MessageId( peerId: parsedIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: maxId ) } else { resolvedMaxMessage = nil } var isChannelPost = false for attribute in topMessage.attributes { if let _ = attribute as? SourceReferenceMessageAttribute { isChannelPost = true break } } return DiscussionMessage( messageId: parsedIndex.id, channelMessageId: channelMessageId, isChannelPost: isChannelPost, maxMessage: resolvedMaxMessage, maxReadIncomingMessageId: readInboxMaxId.flatMap { readMaxId in MessageId(peerId: parsedIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: readMaxId) }, maxReadOutgoingMessageId: readOutboxMaxId.flatMap { readMaxId in MessageId(peerId: parsedIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: readMaxId) } ) } } } let discussionMessageSignal = (replyInfo.get() |> take(1) |> mapToSignal { replyInfo -> Signal in guard let replyInfo = replyInfo else { return .single(nil) } return account.postbox.transaction { transaction -> DiscussionMessage? in var foundDiscussionMessageId: MessageId? transaction.scanMessageAttributes(peerId: replyInfo.commentsPeerId, namespace: Namespaces.Message.Cloud, limit: 1000, { id, attributes in for attribute in attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { if attribute.messageId == messageId { foundDiscussionMessageId = id return true } } } if foundDiscussionMessageId != nil { return false } return true }) guard let discussionMessageId = foundDiscussionMessageId else { return nil } return DiscussionMessage( messageId: discussionMessageId, channelMessageId: messageId, isChannelPost: true, maxMessage: replyInfo.maxMessageId, maxReadIncomingMessageId: replyInfo.maxReadIncomingMessageId, maxReadOutgoingMessageId: nil ) } }) |> mapToSignal { result -> Signal in if let result = result { return .single(result) } else { return remoteDiscussionMessageSignal } } let discussionMessage = Promise() discussionMessage.set(discussionMessageSignal) enum Anchor { case message(MessageId) case lowerBound case upperBound } let preloadedHistoryPosition: Signal<(FetchMessageHistoryHoleThreadInput, PeerId, MessageId?, Anchor, MessageId?), FetchChannelReplyThreadMessageError> = replyInfo.get() |> take(1) |> castError(FetchChannelReplyThreadMessageError.self) |> mapToSignal { replyInfo -> Signal<(FetchMessageHistoryHoleThreadInput, PeerId, MessageId?, Anchor, MessageId?), FetchChannelReplyThreadMessageError> in if let replyInfo = replyInfo { return account.postbox.transaction { transaction -> (FetchMessageHistoryHoleThreadInput, PeerId, MessageId?, Anchor, MessageId?) in var threadInput: FetchMessageHistoryHoleThreadInput = .threadFromChannel(channelMessageId: messageId) var threadMessageId: MessageId? transaction.scanMessageAttributes(peerId: replyInfo.commentsPeerId, namespace: Namespaces.Message.Cloud, limit: 1000, { id, attributes in for attribute in attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { if attribute.messageId == messageId { threadMessageId = id threadInput = .direct(peerId: id.peerId, threadId: makeMessageThreadId(id)) return false } } } return true }) let anchor: Anchor if let atMessageId = atMessageId { anchor = .message(atMessageId) } else if let maxReadIncomingMessageId = replyInfo.maxReadIncomingMessageId { anchor = .message(maxReadIncomingMessageId) } else { anchor = .lowerBound } return (threadInput, replyInfo.commentsPeerId, threadMessageId, anchor, replyInfo.maxMessageId) } |> castError(FetchChannelReplyThreadMessageError.self) } else { return discussionMessage.get() |> take(1) |> castError(FetchChannelReplyThreadMessageError.self) |> mapToSignal { discussionMessage -> Signal<(FetchMessageHistoryHoleThreadInput, PeerId, MessageId?, Anchor, MessageId?), FetchChannelReplyThreadMessageError> in guard let discussionMessage = discussionMessage else { return .fail(.generic) } let topMessageId = discussionMessage.messageId let commentsPeerId = topMessageId.peerId let anchor: Anchor if let atMessageId = atMessageId { anchor = .message(atMessageId) } else if let maxReadIncomingMessageId = discussionMessage.maxReadIncomingMessageId { anchor = .message(maxReadIncomingMessageId) } else { anchor = .lowerBound } return .single((.direct(peerId: commentsPeerId, threadId: makeMessageThreadId(topMessageId)), commentsPeerId, discussionMessage.messageId, anchor, discussionMessage.maxMessage)) } } } let preloadedHistory = preloadedHistoryPosition |> mapToSignal { peerInput, commentsPeerId, threadMessageId, anchor, maxMessageId -> Signal<(FetchMessageHistoryHoleResult?, ChatReplyThreadMessage.Anchor), FetchChannelReplyThreadMessageError> in guard let maxMessageId = maxMessageId else { return .single((FetchMessageHistoryHoleResult(removedIndices: IndexSet(integersIn: 1 ..< Int(Int32.max - 1)), strictRemovedIndices: IndexSet(), actualPeerId: nil, actualThreadId: nil), .automatic)) } return account.postbox.transaction { transaction -> Signal<(FetchMessageHistoryHoleResult?, ChatReplyThreadMessage.Anchor), FetchChannelReplyThreadMessageError> in if let threadMessageId = threadMessageId { var holes = transaction.getThreadIndexHoles(peerId: threadMessageId.peerId, threadId: makeMessageThreadId(threadMessageId), namespace: Namespaces.Message.Cloud) holes.remove(integersIn: Int(maxMessageId.id + 1) ..< Int(Int32.max)) let isParticipant = transaction.getPeerChatListIndex(commentsPeerId) != nil if isParticipant { let historyHoles = transaction.getHoles(peerId: commentsPeerId, namespace: Namespaces.Message.Cloud) holes.formIntersection(historyHoles) } let inputAnchor: HistoryViewInputAnchor switch anchor { case .lowerBound: inputAnchor = .lowerBound case .upperBound: inputAnchor = .upperBound case let .message(id): inputAnchor = .message(id) } let testView = transaction.getMessagesHistoryViewState( input: .external(MessageHistoryViewExternalInput( peerId: commentsPeerId, threadId: makeMessageThreadId(threadMessageId), maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, holes: [ Namespaces.Message.Cloud: holes ] )), count: 40, clipHoles: true, anchor: inputAnchor, namespaces: .not(Namespaces.Message.allScheduled) ) if !testView.isLoading { let initialAnchor: ChatReplyThreadMessage.Anchor switch anchor { case .lowerBound: if let entry = testView.entries.first { initialAnchor = .lowerBoundMessage(entry.index) } else { initialAnchor = .automatic } case .upperBound: initialAnchor = .automatic case .message: initialAnchor = .automatic } return .single((FetchMessageHistoryHoleResult(removedIndices: IndexSet(), strictRemovedIndices: IndexSet(), actualPeerId: nil, actualThreadId: nil), initialAnchor)) } } let direction: MessageHistoryViewRelativeHoleDirection switch anchor { case .lowerBound: direction = .range(start: MessageId(peerId: commentsPeerId, namespace: Namespaces.Message.Cloud, id: 1), end: MessageId(peerId: commentsPeerId, namespace: Namespaces.Message.Cloud, id: Int32.max - 1)) case .upperBound: direction = .range(start: MessageId(peerId: commentsPeerId, namespace: Namespaces.Message.Cloud, id: Int32.max - 1), end: MessageId(peerId: commentsPeerId, namespace: Namespaces.Message.Cloud, id: 1)) case let .message(id): direction = .aroundId(id) } return fetchMessageHistoryHole(accountPeerId: account.peerId, source: .network(account.network), postbox: account.postbox, peerInput: peerInput, namespace: Namespaces.Message.Cloud, direction: direction, space: .everywhere, count: 40) |> castError(FetchChannelReplyThreadMessageError.self) |> mapToSignal { result -> Signal<(FetchMessageHistoryHoleResult?, ChatReplyThreadMessage.Anchor), FetchChannelReplyThreadMessageError> in return account.postbox.transaction { transaction -> (FetchMessageHistoryHoleResult?, ChatReplyThreadMessage.Anchor) in guard let result = result else { return (nil, .automatic) } let initialAnchor: ChatReplyThreadMessage.Anchor switch anchor { case .lowerBound: if let actualPeerId = result.actualPeerId, let actualThreadId = result.actualThreadId { if let firstMessage = transaction.getMessagesWithThreadId(peerId: actualPeerId, namespace: Namespaces.Message.Cloud, threadId: actualThreadId, from: MessageIndex.lowerBound(peerId: actualPeerId, namespace: Namespaces.Message.Cloud), includeFrom: false, to: MessageIndex.upperBound(peerId: actualPeerId, namespace: Namespaces.Message.Cloud), limit: 1).first { initialAnchor = .lowerBoundMessage(firstMessage.index) } else { initialAnchor = .automatic } } else { initialAnchor = .automatic } case .upperBound: initialAnchor = .automatic case .message: initialAnchor = .automatic } return (result, initialAnchor) } |> castError(FetchChannelReplyThreadMessageError.self) } } |> castError(FetchChannelReplyThreadMessageError.self) |> switchToLatest } return combineLatest( discussionMessage.get() |> take(1) |> castError(FetchChannelReplyThreadMessageError.self), preloadedHistory ) |> mapToSignal { discussionMessage, initialFilledHolesAndInitialAnchor -> Signal in guard let discussionMessage = discussionMessage else { return .fail(.generic) } let (initialFilledHoles, initialAnchor) = initialFilledHolesAndInitialAnchor return account.postbox.transaction { transaction -> Signal in if let initialFilledHoles = initialFilledHoles { for range in initialFilledHoles.strictRemovedIndices.rangeView { transaction.removeThreadIndexHole(peerId: discussionMessage.messageId.peerId, threadId: makeMessageThreadId(discussionMessage.messageId), namespace: Namespaces.Message.Cloud, space: .everywhere, range: Int32(range.lowerBound) ... Int32(range.upperBound)) } } return .single(ChatReplyThreadMessage( messageId: discussionMessage.messageId, channelMessageId: discussionMessage.channelMessageId, isChannelPost: discussionMessage.isChannelPost, maxMessage: discussionMessage.maxMessage, maxReadIncomingMessageId: discussionMessage.maxReadIncomingMessageId, maxReadOutgoingMessageId: discussionMessage.maxReadOutgoingMessageId, initialFilledHoles: initialFilledHoles?.removedIndices ?? IndexSet(), initialAnchor: initialAnchor, isNotAvailable: initialFilledHoles == nil )) } |> castError(FetchChannelReplyThreadMessageError.self) |> switchToLatest } } }