Swiftgram/TelegramUI/ChatController.swift
2016-09-05 23:21:13 +03:00

1017 lines
51 KiB
Swift

import Foundation
import UIKit
import Postbox
import SwiftSignalKit
import Display
import AsyncDisplayKit
import TelegramCore
private enum ChatControllerScrollPosition {
case Unread(index: MessageIndex)
case Index(index: MessageIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool)
}
private enum ChatHistoryViewUpdateType {
case Initial(fadeIn: Bool)
case Generic(type: ViewUpdateType)
}
private enum ChatHistoryViewUpdate {
case Loading
case HistoryView(view: MessageHistoryView, type: ChatHistoryViewUpdateType, scrollPosition: ChatControllerScrollPosition?)
}
private struct ChatHistoryView {
let originalView: MessageHistoryView
let filteredEntries: [ChatHistoryEntry]
}
private enum ChatHistoryViewTransitionReason {
case Initial(fadeIn: Bool)
case InteractiveChanges
case HoleChanges(filledHoleDirections: [MessageIndex: HoleFillDirection], removeHoleDirections: [MessageIndex: HoleFillDirection])
case Reload
}
private struct ChatHistoryViewTransition {
let historyView: ChatHistoryView
let deleteItems: [ListViewDeleteItem]
let insertItems: [ListViewInsertItem]
let updateItems: [ListViewUpdateItem]
let options: ListViewDeleteAndInsertOptions
let scrollToItem: ListViewScrollToItem?
let stationaryItemRange: (Int, Int)?
}
private func messageHistoryViewForLocation(_ location: ChatHistoryLocation, account: Account, peerId: PeerId, fixedCombinedReadState: CombinedPeerReadState?, tagMask: MessageTags?) -> Signal<ChatHistoryViewUpdate, NoError> {
switch location {
case let .Initial(count):
var preloaded = false
var fadeIn = false
return account.viewTracker.aroundUnreadMessageHistoryViewForPeerId(peerId, count: count, tagMask: tagMask) |> map { view, updateType -> ChatHistoryViewUpdate in
if preloaded {
return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil)
} else {
if let maxReadIndex = view.maxReadIndex {
var targetIndex = 0
for i in 0 ..< view.entries.count {
if view.entries[i].index >= maxReadIndex {
targetIndex = i
break
}
}
let maxIndex = min(view.entries.count, targetIndex + count / 2)
if maxIndex >= targetIndex {
for i in targetIndex ..< maxIndex {
if case .HoleEntry = view.entries[i] {
fadeIn = true
return .Loading
}
}
}
preloaded = true
return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .Unread(index: maxReadIndex))
} else {
preloaded = true
return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: nil)
}
}
}
case let .InitialSearch(messageId, count):
var preloaded = false
var fadeIn = false
return account.viewTracker.aroundIdMessageHistoryViewForPeerId(peerId, count: count, messageId: messageId, tagMask: tagMask) |> map { view, updateType -> ChatHistoryViewUpdate in
if preloaded {
return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil)
} else {
let anchorIndex = view.anchorIndex
var targetIndex = 0
for i in 0 ..< view.entries.count {
if view.entries[i].index >= anchorIndex {
targetIndex = i
break
}
}
let maxIndex = min(view.entries.count, targetIndex + count / 2)
if maxIndex >= targetIndex {
for i in targetIndex ..< maxIndex {
if case .HoleEntry = view.entries[i] {
fadeIn = true
return .Loading
}
}
}
preloaded = true
//case Index(index: MessageIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool)
return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .Index(index: anchorIndex, position: .Center(.Bottom), directionHint: .Down, animated: false))
}
}
case let .Navigation(index, anchorIndex):
trace("messageHistoryViewForLocation navigation \(index.id.id)")
var first = true
return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask) |> map { view, updateType -> ChatHistoryViewUpdate in
let genericType: ViewUpdateType
if first {
first = false
genericType = ViewUpdateType.UpdateVisible
} else {
genericType = updateType
}
return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil)
}
case let .Scroll(index, anchorIndex, sourceIndex, scrollPosition, animated):
let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up
let chatScrollPosition = ChatControllerScrollPosition.Index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated)
var first = true
return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask) |> map { view, updateType -> ChatHistoryViewUpdate in
let genericType: ViewUpdateType
let scrollPosition: ChatControllerScrollPosition? = first ? chatScrollPosition : nil
if first {
first = false
genericType = ViewUpdateType.UpdateVisible
} else {
genericType = updateType
}
return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: scrollPosition)
}
}
}
private func historyEntriesForView(_ view: MessageHistoryView) -> [ChatHistoryEntry] {
var entries: [ChatHistoryEntry] = []
for entry in view.entries {
switch entry {
case let .HoleEntry(hole, _):
entries.append(.HoleEntry(hole))
case let .MessageEntry(message, _):
entries.append(.MessageEntry(message))
}
}
if let maxReadIndex = view.maxReadIndex {
var inserted = false
var i = 0
let unreadEntry: ChatHistoryEntry = .UnreadEntry(maxReadIndex)
for entry in entries {
if entry > unreadEntry {
entries.insert(unreadEntry, at: i)
inserted = true
break
}
i += 1
}
if !inserted {
//entries.append(.UnreadEntry(maxReadIndex))
}
}
return entries
}
private func preparedHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatControllerScrollPosition?) -> Signal<ChatHistoryViewTransition, NoError> {
return Signal { subscriber in
let updateIndices: [(Int, ChatHistoryEntry)] = []
//let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries)
let (deleteIndices, indicesAndItems) = mergeListsStable(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries)
var adjustedDeleteIndices: [ListViewDeleteItem] = []
let previousCount: Int
if let fromView = fromView {
previousCount = fromView.filteredEntries.count
} else {
previousCount = 0;
}
for index in deleteIndices {
adjustedDeleteIndices.append(ListViewDeleteItem(index: previousCount - 1 - index, directionHint: nil))
}
var adjustedIndicesAndItems: [ListViewInsertItem] = []
var adjustedUpdateItems: [ListViewUpdateItem] = []
let updatedCount = toView.filteredEntries.count
var options: ListViewDeleteAndInsertOptions = []
var maxAnimatedInsertionIndex = -1
var stationaryItemRange: (Int, Int)?
var scrollToItem: ListViewScrollToItem?
switch reason {
case let .Initial(fadeIn):
if fadeIn {
let _ = options.insert(.AnimateAlpha)
} else {
let _ = options.insert(.LowLatency)
let _ = options.insert(.Synchronous)
}
case .InteractiveChanges:
let _ = options.insert(.AnimateAlpha)
let _ = options.insert(.AnimateInsertion)
for (index, _, _) in indicesAndItems.sorted(by: { $0.0 > $1.0 }) {
let adjustedIndex = updatedCount - 1 - index
if adjustedIndex == maxAnimatedInsertionIndex + 1 {
maxAnimatedInsertionIndex += 1
}
}
case .Reload:
break
case let .HoleChanges(filledHoleDirections, removeHoleDirections):
if let (_, removeDirection) = removeHoleDirections.first {
switch removeDirection {
case .LowerToUpper:
var holeIndex: MessageIndex?
for (index, _) in filledHoleDirections {
if holeIndex == nil || index < holeIndex! {
holeIndex = index
}
}
if let holeIndex = holeIndex {
for i in 0 ..< toView.filteredEntries.count {
if toView.filteredEntries[i].index >= holeIndex {
let index = toView.filteredEntries.count - 1 - (i - 1)
stationaryItemRange = (index, Int.max)
break
}
}
}
case .UpperToLower:
break
case .AroundIndex:
break
}
}
}
for (index, entry, previousIndex) in indicesAndItems {
let adjustedIndex = updatedCount - 1 - index
let adjustedPrevousIndex: Int?
if let previousIndex = previousIndex {
adjustedPrevousIndex = previousCount - 1 - previousIndex
} else {
adjustedPrevousIndex = nil
}
var directionHint: ListViewItemOperationDirectionHint?
if maxAnimatedInsertionIndex >= 0 && adjustedIndex <= maxAnimatedInsertionIndex {
directionHint = .Down
}
switch entry {
case let .MessageEntry(message):
adjustedIndicesAndItems.append(ListViewInsertItem(index: adjustedIndex, previousIndex: adjustedPrevousIndex, item: ChatMessageItem(account: account, peerId: peerId, controllerInteraction: controllerInteraction, message: message), directionHint: directionHint))
case .HoleEntry:
adjustedIndicesAndItems.append(ListViewInsertItem(index: adjustedIndex, previousIndex: adjustedPrevousIndex, item: ChatHoleItem(), directionHint: directionHint))
case .UnreadEntry:
adjustedIndicesAndItems.append(ListViewInsertItem(index: adjustedIndex, previousIndex: adjustedPrevousIndex, item: ChatUnreadItem(), directionHint: directionHint))
}
}
for (index, entry) in updateIndices {
let adjustedIndex = updatedCount - 1 - index
let directionHint: ListViewItemOperationDirectionHint? = nil
switch entry {
case let .MessageEntry(message):
adjustedUpdateItems.append(ListViewUpdateItem(index: adjustedIndex, item: ChatMessageItem(account: account, peerId: peerId, controllerInteraction: controllerInteraction, message: message), directionHint: directionHint))
case .HoleEntry:
adjustedUpdateItems.append(ListViewUpdateItem(index: adjustedIndex, item: ChatHoleItem(), directionHint: directionHint))
case .UnreadEntry:
adjustedUpdateItems.append(ListViewUpdateItem(index: adjustedIndex, item: ChatUnreadItem(), directionHint: directionHint))
}
}
if let scrollPosition = scrollPosition {
switch scrollPosition {
case let .Unread(unreadIndex):
var index = toView.filteredEntries.count - 1
for entry in toView.filteredEntries {
if case .UnreadEntry = entry {
scrollToItem = ListViewScrollToItem(index: index, position: .Bottom, animated: false, curve: .Default, directionHint: .Down)
break
}
index -= 1
}
if scrollToItem == nil {
var index = toView.filteredEntries.count - 1
for entry in toView.filteredEntries {
if entry.index >= unreadIndex {
scrollToItem = ListViewScrollToItem(index: index, position: .Bottom, animated: false, curve: .Default, directionHint: .Down)
break
}
index -= 1
}
}
if scrollToItem == nil {
var index = 0
for entry in toView.filteredEntries.reversed() {
if entry.index < unreadIndex {
scrollToItem = ListViewScrollToItem(index: index, position: .Bottom, animated: false, curve: .Default, directionHint: .Down)
break
}
index += 1
}
}
case let .Index(scrollIndex, position, directionHint, animated):
var index = toView.filteredEntries.count - 1
for entry in toView.filteredEntries {
if entry.index >= scrollIndex {
scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint)
break
}
index -= 1
}
if scrollToItem == nil {
var index = 0
for entry in toView.filteredEntries.reversed() {
if entry.index < scrollIndex {
scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint)
break
}
index += 1
}
}
}
}
subscriber.putNext(ChatHistoryViewTransition(historyView: toView, deleteItems: adjustedDeleteIndices, insertItems: adjustedIndicesAndItems, updateItems: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange))
subscriber.putCompletion()
return EmptyDisposable
}
}
private func maxIncomingMessageIdForEntries(_ entries: [ChatHistoryEntry], indexRange: (Int, Int)) -> MessageId? {
for i in (indexRange.0 ... indexRange.1).reversed() {
if case let .MessageEntry(message) = entries[i], message.flags.contains(.Incoming) {
return message.id
}
}
return nil
}
private var useDarkMode = false
public class ChatController: ViewController {
private var containerLayout = ContainerViewLayout()
private let account: Account
private let peerId: PeerId
private let messageId: MessageId?
private var historyView: ChatHistoryView?
private let peerDisposable = MetaDisposable()
private let historyDisposable = MetaDisposable()
private let readHistoryDisposable = MetaDisposable()
private let messageViewQueue = Queue()
private let messageIndexDisposable = MetaDisposable()
private var enqueuedHistoryViewTransition: (ChatHistoryViewTransition, () -> Void)?
private var layoutActionOnViewTransition: (@escaping () -> Void)?
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
private var didSetReady = false
private let maxVisibleIncomingMessageId = Promise<MessageId>()
private let canReadHistory = Promise<Bool>()
private let _chatHistoryLocation = Promise<ChatHistoryLocation>()
private var chatHistoryLocation: Signal<ChatHistoryLocation, NoError> {
return self._chatHistoryLocation.get()
}
private var presentationInterfaceState = ChatPresentationInterfaceState(interfaceState: ChatInterfaceState(), peer: nil)
private let chatInterfaceStatePromise = Promise<ChatInterfaceState>()
private var leftNavigationButton: ChatNavigationButton?
private var rightNavigationButton: ChatNavigationButton?
private var chatInfoNavigationButton: ChatNavigationButton?
private let galleryHiddenMesageAndMediaDisposable = MetaDisposable()
private var controllerInteraction: ChatControllerInteraction?
public init(account: Account, peerId: PeerId, messageId: MessageId? = nil) {
self.account = account
self.peerId = peerId
self.messageId = messageId
super.init()
self.setupThemeWithDarkMode(useDarkMode)
self.scrollToTop = { [weak self] in
if let strongSelf = self {
strongSelf._chatHistoryLocation.set(.single(ChatHistoryLocation.Scroll(index: MessageIndex.lowerBound(peerId: strongSelf.peerId), anchorIndex: MessageIndex.lowerBound(peerId: strongSelf.peerId), sourceIndex: MessageIndex.upperBound(peerId: strongSelf.peerId), scrollPosition: .Bottom, animated: true)))
}
}
let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] id in
if let strongSelf = self, let historyView = strongSelf.historyView {
var galleryMedia: Media?
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id {
for media in message.media {
if let file = media as? TelegramMediaFile {
galleryMedia = file
} else if let image = media as? TelegramMediaImage {
galleryMedia = image
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
if let file = content.file {
galleryMedia = file
} else if let image = content.image {
galleryMedia = image
}
}
}
break
}
if let galleryMedia = galleryMedia {
if let file = galleryMedia as? TelegramMediaFile, file.mimeType == "audio/mpeg" {
//debugPlayMedia(account: strongSelf.account, file: file)
} else {
let gallery = GalleryController(account: strongSelf.account, messageId: id)
strongSelf.galleryHiddenMesageAndMediaDisposable.set(gallery.hiddenMedia.start(next: { [weak strongSelf] messageIdAndMedia in
if let strongSelf = strongSelf {
if let messageIdAndMedia = messageIdAndMedia {
strongSelf.controllerInteraction?.hiddenMedia = [messageIdAndMedia.0: [messageIdAndMedia.1]]
} else {
strongSelf.controllerInteraction?.hiddenMedia = [:]
}
strongSelf.chatDisplayNode.listView.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
itemNode.updateHiddenMedia()
}
}
}
}))
strongSelf.present(gallery, in: .window, with: GalleryControllerPresentationArguments(transitionNode: { [weak self] messageId, media in
if let strongSelf = self {
var transitionNode: ASDisplayNode?
strongSelf.chatDisplayNode.listView.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
if let result = itemNode.transitionNode(id: messageId, media: media) {
transitionNode = result
}
}
}
return transitionNode
}
return nil
}))
}
}
}
}, openPeer: { [weak self] id, navigation in
if let strongSelf = self {
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: id, messageId: nil))
}
}, openMessageContextMenu: { [weak self] id, node, frame in
if let strongSelf = self, let historyView = strongSelf.historyView {
let contextMenuController = ContextMenuController(actions: [
ContextMenuAction(content: .text("Reply"), action: { [weak strongSelf] in
if let strongSelf = strongSelf, let historyView = strongSelf.historyView {
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id {
strongSelf.updateChatInterfaceState(strongSelf.chatInterfaceState.withUpdatedReplyMessageId(message.id), animated: true)
strongSelf.chatDisplayNode.ensureInputViewFocused()
break
}
}
}),
ContextMenuAction(content: .text("Copy"), action: { [weak strongSelf] in
if let strongSelf = strongSelf, let historyView = strongSelf.historyView {
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id {
if !message.text.isEmpty {
UIPasteboard.general.string = message.text
}
break
}
}
}),
ContextMenuAction(content: .text("More..."), action: { [weak strongSelf] in
if let strongSelf = strongSelf, let historyView = strongSelf.historyView {
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id {
if strongSelf.chatInterfaceState.selectionState != nil {
strongSelf.updateChatInterfaceState(strongSelf.chatInterfaceState.withoutSelectionState(), animated: true)
} else {
strongSelf.updateChatInterfaceState(strongSelf.chatInterfaceState.withUpdatedSelectedMessage(message.id), animated: true)
}
break
}
}
})
])
strongSelf.present(contextMenuController, in: .window, with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak strongSelf, weak node] in
if let node = node {
return (node, frame)
} else {
return nil
}
}))
}
}, navigateToMessage: { [weak self] fromId, id in
if let strongSelf = self, let historyView = strongSelf.historyView {
if id.peerId == strongSelf.peerId {
var fromIndex: MessageIndex?
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == fromId {
fromIndex = MessageIndex(message)
break
}
if let fromIndex = fromIndex {
var found = false
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id {
found = true
strongSelf._chatHistoryLocation.set(.single(ChatHistoryLocation.Scroll(index: MessageIndex(message), anchorIndex: MessageIndex(message), sourceIndex: fromIndex, scrollPosition: .Center(.Bottom), animated: true)))
}
if !found {
strongSelf.messageIndexDisposable.set((strongSelf.account.postbox.messageIndexAtId(id) |> deliverOnMainQueue).start(next: { [weak strongSelf] index in
if let strongSelf = strongSelf, let index = index {
strongSelf._chatHistoryLocation.set(.single(ChatHistoryLocation.Scroll(index:index, anchorIndex: index, sourceIndex: fromIndex, scrollPosition: .Center(.Bottom), animated: true)))
}
}))
}
}
} else {
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: id.peerId, messageId: id))
}
}
}, clickThroughMessage: { [weak self] in
self?.view.endEditing(true)
}, toggleMessageSelection: { [weak self] messageId in
if let strongSelf = self, let historyView = strongSelf.historyView {
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == messageId {
strongSelf.updateChatInterfaceState(strongSelf.chatInterfaceState.withToggledSelectedMessage(messageId), animated: false)
break
}
}
})
self.controllerInteraction = controllerInteraction
let messageViewQueue = self.messageViewQueue
self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo, buttonItem: UIBarButtonItem(customDisplayNode: ChatAvatarNavigationNode()))
self.updateChatInterfaceState(self.chatInterfaceState, animated: false)
peerDisposable.set((account.postbox.peerWithId(peerId)
|> deliverOnMainQueue).start(next: { [weak self] peer in
if let strongSelf = self {
strongSelf.title = peer.displayTitle
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer)
}
}))
let fixedCombinedReadState = Atomic<CombinedPeerReadState?>(value: nil)
let historyViewUpdate = self.chatHistoryLocation
|> distinctUntilChanged
|> mapToSignal { location in
return messageHistoryViewForLocation(location, account: account, peerId: peerId, fixedCombinedReadState: fixedCombinedReadState.with { $0 }, tagMask: nil) |> beforeNext { viewUpdate in
switch viewUpdate {
case let .HistoryView(view, _, _):
let _ = fixedCombinedReadState.swap(view.combinedReadState)
default:
break
}
}
}
let previousView = Atomic<ChatHistoryView?>(value: nil)
let historyViewTransition = historyViewUpdate |> mapToQueue { [weak self] update -> Signal<ChatHistoryViewTransition, NoError> in
switch update {
case .Loading:
Queue.mainQueue().async { [weak self] in
if let strongSelf = self {
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(.single(true))
}
}
}
return .complete()
case let .HistoryView(view, type, scrollPosition):
let reason: ChatHistoryViewTransitionReason
var prepareOnMainQueue = false
switch type {
case let .Initial(fadeIn):
reason = ChatHistoryViewTransitionReason.Initial(fadeIn: fadeIn)
prepareOnMainQueue = !fadeIn
case let .Generic(genericType):
switch genericType {
case .InitialUnread:
reason = ChatHistoryViewTransitionReason.Initial(fadeIn: false)
case .Generic:
reason = ChatHistoryViewTransitionReason.InteractiveChanges
case .UpdateVisible:
reason = ChatHistoryViewTransitionReason.Reload
case let .FillHole(insertions, deletions):
reason = ChatHistoryViewTransitionReason.HoleChanges(filledHoleDirections: insertions, removeHoleDirections: deletions)
}
}
let processedView = ChatHistoryView(originalView: view, filteredEntries: historyEntriesForView(view))
let previous = previousView.swap(processedView)
return preparedHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition) |> runOn( prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue)
}
}
let appliedTransition = historyViewTransition |> deliverOnMainQueue |> mapToQueue { [weak self] transition -> Signal<Void, NoError> in
if let strongSelf = self {
return strongSelf.enqueueHistoryViewTransition(transition)
}
return .complete()
}
self.historyDisposable.set(appliedTransition.start())
let previousMaxIncomingMessageId = Atomic<MessageId?>(value: nil)
let readHistory = combineLatest(self.maxVisibleIncomingMessageId.get(), self.canReadHistory.get())
|> map { messageId, canRead in
if canRead {
var apply = false
let _ = previousMaxIncomingMessageId.modify { previousId in
if previousId == nil || previousId! < messageId {
apply = true
return messageId
} else {
return previousId
}
}
if apply {
let _ = account.postbox.modify({ modifier in
modifier.applyInteractiveReadMaxId(messageId)
}).start()
}
}
}
self.readHistoryDisposable.set(readHistory.start())
if let messageId = messageId {
self._chatHistoryLocation.set(.single(ChatHistoryLocation.InitialSearch(messageId: messageId, count: 60)))
} else {
self._chatHistoryLocation.set(.single(ChatHistoryLocation.Initial(count: 60)))
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.historyDisposable.dispose()
self.readHistoryDisposable.dispose()
self.messageIndexDisposable.dispose()
self.galleryHiddenMesageAndMediaDisposable.dispose()
}
private func setupThemeWithDarkMode(_ darkMode: Bool) {
if darkMode {
self.statusBar.style = .White
self.navigationBar.backgroundColor = UIColor(white: 0.0, alpha: 0.9)
self.navigationBar.foregroundColor = UIColor.white
self.navigationBar.accentColor = UIColor.white
self.navigationBar.stripeColor = UIColor.black
} else {
self.statusBar.style = .Black
self.navigationBar.backgroundColor = UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0)
self.navigationBar.foregroundColor = UIColor.black
self.navigationBar.accentColor = UIColor(0x1195f2)
self.navigationBar.stripeColor = UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0)
}
}
var chatDisplayNode: ChatControllerNode {
get {
return super.displayNode as! ChatControllerNode
}
}
override public func loadDisplayNode() {
self.displayNode = ChatControllerNode(account: self.account, peerId: self.peerId)
self.chatDisplayNode.listView.displayedItemRangeChanged = { [weak self] displayedRange in
if let strongSelf = self {
/*if let transactionTag = strongSelf.listViewTransactionTag {
strongSelf.messageViewQueue.dispatch {
if transactionTag == strongSelf.historyViewTransactionTag {
if let range = range, historyView = strongSelf.historyView, firstEntry = historyView.filteredEntries.first, lastEntry = historyView.filteredEntries.last {
if range.firstIndex < 5 && historyView.originalView.laterId != nil {
strongSelf._chatHistoryLocation.set(.single(ChatHistoryLocation.Navigation(index: lastEntry.index, anchorIndex: historyView.originalView.anchorIndex)))
} else if range.lastIndex >= historyView.filteredEntries.count - 5 && historyView.originalView.earlierId != nil {
strongSelf._chatHistoryLocation.set(.single(ChatHistoryLocation.Navigation(index: firstEntry.index, anchorIndex: historyView.originalView.anchorIndex)))
} else {
//strongSelf.account.postbox.updateMessageHistoryViewVisibleRange(messageView.id, earliestVisibleIndex: viewEntries[viewEntries.count - 1 - range.lastIndex].index, latestVisibleIndex: viewEntries[viewEntries.count - 1 - range.firstIndex].index)
}
}
}
}
}*/
if let visible = displayedRange.visibleRange, let historyView = strongSelf.historyView {
if let messageId = maxIncomingMessageIdForEntries(historyView.filteredEntries, indexRange: (historyView.filteredEntries.count - 1 - visible.lastIndex, historyView.filteredEntries.count - 1 - visible.firstIndex)) {
strongSelf.updateMaxVisibleReadIncomingMessageId(messageId)
}
}
}
}
self.chatDisplayNode.listView.visibleContentOffsetChanged = { [weak self] offset in
if let strongSelf = self {
let offsetAlpha: CGFloat
switch offset {
case let .known(offset):
if offset < 40.0 {
offsetAlpha = 0.0
} else {
offsetAlpha = 1.0
}
case .unknown:
offsetAlpha = 1.0
case .none:
offsetAlpha = 0.0
}
if !strongSelf.chatDisplayNode.navigateToLatestButton.alpha.isEqual(to: offsetAlpha) {
UIView.animate(withDuration: 0.2, delay: 0.0, options: [.beginFromCurrentState], animations: {
strongSelf.chatDisplayNode.navigateToLatestButton.alpha = offsetAlpha
}, completion: nil)
}
}
}
self.chatDisplayNode.requestLayout = { [weak self] transition in
self?.requestLayout(transition: transition)
}
self.chatDisplayNode.setupSendActionOnViewUpdate = { [weak self] f in
self?.layoutActionOnViewTransition = f
}
self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] state, animated in
self?.updateChatInterfaceState(state, animated: animated)
}
self.chatDisplayNode.displayAttachmentMenu = { [weak self] in
if let strongSelf = self {
let controller = ChatMediaActionSheetController()
controller.location = { [weak strongSelf] in
if let strongSelf = strongSelf {
let mapInputController = MapInputController()
strongSelf.present(mapInputController, in: .window)
}
}
controller.contacts = { [weak strongSelf] in
if let strongSelf = strongSelf {
useDarkMode = !useDarkMode
strongSelf.setupThemeWithDarkMode(useDarkMode)
}
}
strongSelf.present(controller, in: .window)
}
}
self.chatDisplayNode.navigateToLatestButton.tapped = { [weak self] in
if let strongSelf = self {
strongSelf._chatHistoryLocation.set(.single(ChatHistoryLocation.Scroll(index: MessageIndex.upperBound(peerId: strongSelf.peerId), anchorIndex: MessageIndex.upperBound(peerId: strongSelf.peerId), sourceIndex: MessageIndex.lowerBound(peerId: strongSelf.peerId), scrollPosition: .Top, animated: true)))
}
}
self.chatDisplayNode.interfaceInteraction = ChatPanelInterfaceInteraction(deleteSelectedMessages: { [weak self] in
if let strongSelf = self {
if let messageIds = strongSelf.chatInterfaceState.selectionState?.selectedIds, !messageIds.isEmpty {
strongSelf.account.postbox.modify({ modifier in
modifier.deleteMessages(Array(messageIds))
}).start()
}
strongSelf.updateChatInterfaceState(strongSelf.chatInterfaceState.withoutSelectionState(), animated: true)
}
}, forwardSelectedMessages: { [weak self] in
if let strongSelf = self {
let controller = ShareRecipientsActionSheetController()
strongSelf.present(controller, in: .window)
}
})
self.displayNodeDidLoad()
self.dequeueHistoryViewTransition()
}
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.chatDisplayNode.listView.preloadPages = true
self.canReadHistory.set(.single(true))
}
private func enqueueHistoryViewTransition(_ transition: ChatHistoryViewTransition) -> Signal<Void, NoError> {
return Signal { [weak self] subscriber in
if let strongSelf = self {
if let _ = strongSelf.enqueuedHistoryViewTransition {
preconditionFailure()
}
strongSelf.enqueuedHistoryViewTransition = (transition, {
subscriber.putCompletion()
})
if strongSelf.isNodeLoaded {
strongSelf.dequeueHistoryViewTransition()
} else {
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(.single(true))
}
}
} else {
subscriber.putCompletion()
}
return EmptyDisposable
} |> runOn(Queue.mainQueue())
}
private func updateMaxVisibleReadIncomingMessageId(_ id: MessageId) {
self.maxVisibleIncomingMessageId.set(.single(id))
}
private func dequeueHistoryViewTransition() {
if let (transition, completion) = self.enqueuedHistoryViewTransition {
self.enqueuedHistoryViewTransition = nil
let completion: (ListViewDisplayedItemRange) -> Void = { [weak self] visibleRange in
if let strongSelf = self {
strongSelf.historyView = transition.historyView
if let range = visibleRange.loadedRange {
strongSelf.account.postbox.updateMessageHistoryViewVisibleRange(transition.historyView.originalView.id, earliestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.lastIndex].index, latestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.firstIndex].index)
if let visible = visibleRange.visibleRange {
if let messageId = maxIncomingMessageIdForEntries(transition.historyView.filteredEntries, indexRange: (transition.historyView.filteredEntries.count - 1 - visible.lastIndex, transition.historyView.filteredEntries.count - 1 - visible.firstIndex)) {
strongSelf.updateMaxVisibleReadIncomingMessageId(messageId)
}
}
}
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(.single(true))
}
completion()
}
}
if let layoutActionOnViewTransition = self.layoutActionOnViewTransition {
self.layoutActionOnViewTransition = nil
layoutActionOnViewTransition()
self.chatDisplayNode.containerLayoutUpdated(self.containerLayout, navigationBarHeight: self.navigationBar.frame.maxY, transition: .animated(duration: 0.4, curve: .spring), listViewTransaction: { updateSizeAndInsets in
var options = transition.options
let _ = options.insert(.Synchronous)
let _ = options.insert(.LowLatency)
options.remove(.AnimateInsertion)
let deleteItems = transition.deleteItems.map({ item in
return ListViewDeleteItem(index: item.index, directionHint: nil)
})
var maxInsertedItem: Int?
var insertItems: [ListViewInsertItem] = []
for i in 0 ..< transition.insertItems.count {
let item = transition.insertItems[i]
if item.directionHint == .Down && (maxInsertedItem == nil || maxInsertedItem! < item.index) {
maxInsertedItem = item.index
}
insertItems.append(ListViewInsertItem(index: item.index, previousIndex: item.previousIndex, item: item.item, directionHint: item.directionHint == .Down ? .Up : nil))
}
let scrollToItem = ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Spring(duration: 0.4), directionHint: .Up)
var stationaryItemRange: (Int, Int)?
if let maxInsertedItem = maxInsertedItem {
stationaryItemRange = (maxInsertedItem + 1, Int.max)
}
self.chatDisplayNode.listView.deleteAndInsertItems(deleteIndices: deleteItems, insertIndicesAndItems: insertItems, updateIndicesAndItems: transition.updateItems, options: options, scrollToItem: scrollToItem, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: stationaryItemRange, completion: completion)
})
} else {
self.chatDisplayNode.listView.deleteAndInsertItems(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, completion: completion)
}
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.containerLayout = layout
self.chatDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition, listViewTransaction: { updateSizeAndInsets in
self.chatDisplayNode.listView.deleteAndInsertItems(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, completion: { _ in })
})
}
func updateChatInterfaceState(animated: Bool = true, _ f: (ChatInterfaceState) -> Void) {
if self.isNodeLoaded {
self.chatDisplayNode.updateChatInterfaceState(chatInterfaceState, animated: animated)
}
self.chatInterfaceState = chatInterfaceState
self.chatInterfaceStatePromise.set(.single(chatInterfaceState))
if let button = leftNavigationButtonForChatInterfaceState(chatInterfaceState, currentButton: self.leftNavigationButton, target: self, selector: #selector(self.leftNavigationButtonAction)) {
self.navigationItem.setLeftBarButton(button.buttonItem, animated: true)
self.leftNavigationButton = button
} else if let _ = self.leftNavigationButton {
self.navigationItem.setLeftBarButton(nil, animated: true)
self.leftNavigationButton = nil
}
if let button = rightNavigationButtonForChatInterfaceState(chatInterfaceState, currentButton: self.rightNavigationButton, target: self, selector: #selector(self.rightNavigationButtonAction), chatInfoNavigationButton: self.chatInfoNavigationButton) {
self.navigationItem.setRightBarButton(button.buttonItem, animated: true)
self.rightNavigationButton = button
} else if let _ = self.rightNavigationButton {
self.navigationItem.setRightBarButton(nil, animated: true)
self.rightNavigationButton = nil
}
if let controllerInteraction = self.controllerInteraction {
if chatInterfaceState.selectionState != controllerInteraction.selectionState {
let animated = controllerInteraction.selectionState == nil || chatInterfaceState.selectionState == nil
controllerInteraction.selectionState = chatInterfaceState.selectionState
self.chatDisplayNode.listView.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
itemNode.updateSelectionState(animated: animated)
}
}
}
}
}
@objc func leftNavigationButtonAction() {
if let button = self.leftNavigationButton {
self.navigationButtonAction(button.action)
}
}
@objc func rightNavigationButtonAction() {
if let button = self.rightNavigationButton {
self.navigationButtonAction(button.action)
}
}
private func navigationButtonAction(_ action: ChatNavigationButtonAction) {
switch action {
case .cancelMessageSelection:
self.updateChatInterfaceState(self.chatInterfaceState.withoutSelectionState(), animated: true)
case .clearHistory:
let actionSheet = ActionSheetController()
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: "Delete All Messages", color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: "Cancel", color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
self.present(actionSheet, in: .window)
case .openChatInfo:
break
}
}
}