mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 14:45:21 +00:00
Refactor CallListUI
This commit is contained in:
588
submodules/CallListUI/Sources/CallListControllerNode.swift
Normal file
588
submodules/CallListUI/Sources/CallListControllerNode.swift
Normal file
@@ -0,0 +1,588 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import ItemListUI
|
||||
import AccountContext
|
||||
import TelegramNotices
|
||||
|
||||
private struct CallListNodeListViewTransition {
|
||||
let callListView: CallListNodeView
|
||||
let deleteItems: [ListViewDeleteItem]
|
||||
let insertItems: [ListViewInsertItem]
|
||||
let updateItems: [ListViewUpdateItem]
|
||||
let options: ListViewDeleteAndInsertOptions
|
||||
let scrollToItem: ListViewScrollToItem?
|
||||
let stationaryItemRange: (Int, Int)?
|
||||
}
|
||||
|
||||
private extension CallListViewEntry {
|
||||
var lowestIndex: MessageIndex {
|
||||
switch self {
|
||||
case let .hole(index):
|
||||
return index
|
||||
case let .message(_, messages):
|
||||
var lowest = messages[0].index
|
||||
for i in 1 ..< messages.count {
|
||||
let index = messages[i].index
|
||||
if index < lowest {
|
||||
lowest = index
|
||||
}
|
||||
}
|
||||
return lowest
|
||||
}
|
||||
}
|
||||
|
||||
var highestIndex: MessageIndex {
|
||||
switch self {
|
||||
case let .hole(index):
|
||||
return index
|
||||
case let .message(_, messages):
|
||||
var highest = messages[0].index
|
||||
for i in 1 ..< messages.count {
|
||||
let index = messages[i].index
|
||||
if index > highest {
|
||||
highest = index
|
||||
}
|
||||
}
|
||||
return highest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class CallListNodeInteraction {
|
||||
let setMessageIdWithRevealedOptions: (MessageId?, MessageId?) -> Void
|
||||
let call: (PeerId) -> Void
|
||||
let openInfo: (PeerId, [Message]) -> Void
|
||||
let delete: ([MessageId]) -> Void
|
||||
let updateShowCallsTab: (Bool) -> Void
|
||||
|
||||
init(setMessageIdWithRevealedOptions: @escaping (MessageId?, MessageId?) -> Void, call: @escaping (PeerId) -> Void, openInfo: @escaping (PeerId, [Message]) -> Void, delete: @escaping ([MessageId]) -> Void, updateShowCallsTab: @escaping (Bool) -> Void) {
|
||||
self.setMessageIdWithRevealedOptions = setMessageIdWithRevealedOptions
|
||||
self.call = call
|
||||
self.openInfo = openInfo
|
||||
self.delete = delete
|
||||
self.updateShowCallsTab = updateShowCallsTab
|
||||
}
|
||||
}
|
||||
|
||||
struct CallListNodeState: Equatable {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let dateTimeFormat: PresentationDateTimeFormat
|
||||
let disableAnimations: Bool
|
||||
let editing: Bool
|
||||
let messageIdWithRevealedOptions: MessageId?
|
||||
|
||||
func withUpdatedPresentationData(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, disableAnimations: Bool) -> CallListNodeState {
|
||||
return CallListNodeState(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, disableAnimations: disableAnimations, editing: self.editing, messageIdWithRevealedOptions: self.messageIdWithRevealedOptions)
|
||||
}
|
||||
|
||||
func withUpdatedEditing(_ editing: Bool) -> CallListNodeState {
|
||||
return CallListNodeState(theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, disableAnimations: self.disableAnimations, editing: editing, messageIdWithRevealedOptions: self.messageIdWithRevealedOptions)
|
||||
}
|
||||
|
||||
func withUpdatedMessageIdWithRevealedOptions(_ messageIdWithRevealedOptions: MessageId?) -> CallListNodeState {
|
||||
return CallListNodeState(theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, disableAnimations: self.disableAnimations, editing: self.editing, messageIdWithRevealedOptions: messageIdWithRevealedOptions)
|
||||
}
|
||||
|
||||
static func ==(lhs: CallListNodeState, rhs: CallListNodeState) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.dateTimeFormat != rhs.dateTimeFormat {
|
||||
return false
|
||||
}
|
||||
if lhs.editing != rhs.editing {
|
||||
return false
|
||||
}
|
||||
if lhs.messageIdWithRevealedOptions != rhs.messageIdWithRevealedOptions {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private func mappedInsertEntries(account: Account, showSettings: Bool, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] {
|
||||
return entries.map { entry -> ListViewInsertItem in
|
||||
switch entry.entry {
|
||||
case let .displayTab(theme, text, value):
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(theme: theme, title: text, value: value, enabled: true, sectionId: 0, style: .blocks, updated: { value in
|
||||
nodeInteraction.updateShowCallsTab(value)
|
||||
}), directionHint: entry.directionHint)
|
||||
case let .displayTabInfo(theme, text):
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(theme: theme, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
|
||||
case let .messageEntry(topMessage, messages, theme, strings, dateTimeFormat, editing, hasActiveRevealControls):
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, account: account, style: showSettings ? .blocks : .plain, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, interaction: nodeInteraction), directionHint: entry.directionHint)
|
||||
case let .holeEntry(_, theme):
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListHoleItem(theme: theme), directionHint: entry.directionHint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func mappedUpdateEntries(account: Account, showSettings: Bool, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] {
|
||||
return entries.map { entry -> ListViewUpdateItem in
|
||||
switch entry.entry {
|
||||
case let .displayTab(theme, text, value):
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(theme: theme, title: text, value: value, enabled: true, sectionId: 0, style: .blocks, updated: { value in
|
||||
nodeInteraction.updateShowCallsTab(value)
|
||||
}), directionHint: entry.directionHint)
|
||||
case let .displayTabInfo(theme, text):
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(theme: theme, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
|
||||
case let .messageEntry(topMessage, messages, theme, strings, dateTimeFormat, editing, hasActiveRevealControls):
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, account: account, style: showSettings ? .blocks : .plain, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, interaction: nodeInteraction), directionHint: entry.directionHint)
|
||||
case let .holeEntry(_, theme):
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListHoleItem(theme: theme), directionHint: entry.directionHint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func mappedCallListNodeViewListTransition(account: Account, showSettings: Bool, nodeInteraction: CallListNodeInteraction, transition: CallListNodeViewTransition) -> CallListNodeListViewTransition {
|
||||
return CallListNodeListViewTransition(callListView: transition.callListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, showSettings: showSettings, nodeInteraction: nodeInteraction, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, showSettings: showSettings, nodeInteraction: nodeInteraction, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange)
|
||||
}
|
||||
|
||||
private final class CallListOpaqueTransactionState {
|
||||
let callListView: CallListNodeView
|
||||
|
||||
init(callListView: CallListNodeView) {
|
||||
self.callListView = callListView
|
||||
}
|
||||
}
|
||||
|
||||
final class CallListControllerNode: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
private let mode: CallListControllerMode
|
||||
private var presentationData: PresentationData
|
||||
|
||||
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
private let _ready = ValuePromise<Bool>()
|
||||
private var didSetReady = false
|
||||
var ready: Signal<Bool, NoError> {
|
||||
return _ready.get()
|
||||
}
|
||||
|
||||
var peerSelected: ((PeerId) -> Void)?
|
||||
var activateSearch: (() -> Void)?
|
||||
var deletePeerChat: ((PeerId) -> Void)?
|
||||
|
||||
private let viewProcessingQueue = Queue()
|
||||
private var callListView: CallListNodeView?
|
||||
|
||||
private var dequeuedInitialTransitionOnLayout = false
|
||||
private var enqueuedTransition: (CallListNodeListViewTransition, () -> Void)?
|
||||
|
||||
private var currentState: CallListNodeState
|
||||
private let statePromise: ValuePromise<CallListNodeState>
|
||||
|
||||
private var currentLocationAndType = CallListNodeLocationAndType(location: .initial(count: 50), type: .all)
|
||||
private let callListLocationAndType = ValuePromise<CallListNodeLocationAndType>()
|
||||
private let callListDisposable = MetaDisposable()
|
||||
|
||||
private let listNode: ListView
|
||||
private let emptyTextNode: ASTextNode
|
||||
|
||||
private let call: (PeerId) -> Void
|
||||
private let openInfo: (PeerId, [Message]) -> Void
|
||||
private let emptyStateUpdated: (Bool) -> Void
|
||||
|
||||
private let emptyStatePromise = Promise<Bool>()
|
||||
private let emptyStateDisposable = MetaDisposable()
|
||||
|
||||
init(context: AccountContext, mode: CallListControllerMode, presentationData: PresentationData, call: @escaping (PeerId) -> Void, openInfo: @escaping (PeerId, [Message]) -> Void, emptyStateUpdated: @escaping (Bool) -> Void) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
self.presentationData = presentationData
|
||||
self.call = call
|
||||
self.openInfo = openInfo
|
||||
self.emptyStateUpdated = emptyStateUpdated
|
||||
|
||||
self.currentState = CallListNodeState(theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, disableAnimations: presentationData.disableAnimations, editing: false, messageIdWithRevealedOptions: nil)
|
||||
self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true)
|
||||
|
||||
self.listNode = ListView()
|
||||
self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
|
||||
|
||||
self.emptyTextNode = ASTextNode()
|
||||
self.emptyTextNode.alpha = 0.0
|
||||
self.emptyTextNode.isUserInteractionEnabled = false
|
||||
self.emptyTextNode.displaysAsynchronously = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({
|
||||
return UITracingLayerView()
|
||||
})
|
||||
|
||||
self.addSubnode(self.listNode)
|
||||
self.addSubnode(self.emptyTextNode)
|
||||
|
||||
switch self.mode {
|
||||
case .tab:
|
||||
self.backgroundColor = presentationData.theme.chatList.backgroundColor
|
||||
self.listNode.backgroundColor = presentationData.theme.chatList.backgroundColor
|
||||
case .navigation:
|
||||
self.backgroundColor = presentationData.theme.list.blocksBackgroundColor
|
||||
self.listNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
|
||||
}
|
||||
|
||||
let nodeInteraction = CallListNodeInteraction(setMessageIdWithRevealedOptions: { [weak self] messageId, fromMessageId in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateState { state in
|
||||
if (messageId == nil && fromMessageId == state.messageIdWithRevealedOptions) || (messageId != nil && fromMessageId == nil) {
|
||||
return state.withUpdatedMessageIdWithRevealedOptions(messageId)
|
||||
} else {
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
}, call: { [weak self] peerId in
|
||||
self?.call(peerId)
|
||||
}, openInfo: { [weak self] peerId, messages in
|
||||
self?.openInfo(peerId, messages)
|
||||
}, delete: { [weak self] messageIds in
|
||||
if let strongSelf = self {
|
||||
let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: messageIds, type: .forLocalPeer).start()
|
||||
}
|
||||
}, updateShowCallsTab: { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
let _ = updateCallListSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, {
|
||||
$0.withUpdatedShowTab(value)
|
||||
}).start()
|
||||
|
||||
if value {
|
||||
let _ = ApplicationSpecificNotice.incrementCallsTabTips(accountManager: strongSelf.context.sharedContext.accountManager, count: 4).start()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let viewProcessingQueue = self.viewProcessingQueue
|
||||
|
||||
let callListViewUpdate = self.callListLocationAndType.get()
|
||||
|> distinctUntilChanged
|
||||
|> mapToSignal { locationAndType in
|
||||
return callListViewForLocationAndType(locationAndType: locationAndType, account: context.account)
|
||||
}
|
||||
|
||||
let previousView = Atomic<CallListNodeView?>(value: nil)
|
||||
|
||||
let showSettings: Bool
|
||||
switch mode {
|
||||
case .tab:
|
||||
showSettings = false
|
||||
case .navigation:
|
||||
showSettings = true
|
||||
}
|
||||
|
||||
let showCallsTab = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.callListSettings])
|
||||
|> map { sharedData -> Bool in
|
||||
var value = true
|
||||
if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.callListSettings] as? CallListSettings {
|
||||
value = settings.showTab
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
let callListNodeViewTransition = combineLatest(callListViewUpdate, self.statePromise.get(), showCallsTab) |> mapToQueue { (update, state, showCallsTab) -> Signal<CallListNodeListViewTransition, NoError> in
|
||||
let processedView = CallListNodeView(originalView: update.view, filteredEntries: callListNodeEntriesForView(update.view, state: state, showSettings: showSettings, showCallsTab: showCallsTab))
|
||||
let previous = previousView.swap(processedView)
|
||||
|
||||
let reason: CallListNodeViewTransitionReason
|
||||
var prepareOnMainQueue = false
|
||||
|
||||
var previousWasEmptyOrSingleHole = false
|
||||
if let previous = previous {
|
||||
if previous.filteredEntries.count == 1 {
|
||||
if case .holeEntry = previous.filteredEntries[0] {
|
||||
previousWasEmptyOrSingleHole = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
previousWasEmptyOrSingleHole = true
|
||||
}
|
||||
|
||||
if previousWasEmptyOrSingleHole {
|
||||
reason = .initial
|
||||
if previous == nil {
|
||||
prepareOnMainQueue = true
|
||||
}
|
||||
} else {
|
||||
if previous?.originalView === update.view {
|
||||
reason = .interactiveChanges
|
||||
} else {
|
||||
switch update.type {
|
||||
case .Initial:
|
||||
reason = .initial
|
||||
prepareOnMainQueue = true
|
||||
case .Generic:
|
||||
reason = .interactiveChanges
|
||||
case .UpdateVisible:
|
||||
reason = .reload
|
||||
case .Reload:
|
||||
reason = .reload
|
||||
case .ReloadAnimated:
|
||||
reason = .reloadAnimated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return preparedCallListNodeViewTransition(from: previous, to: processedView, reason: reason, disableAnimations: false, account: context.account, scrollPosition: update.scrollPosition)
|
||||
|> map({ mappedCallListNodeViewListTransition(account: context.account, showSettings: showSettings, nodeInteraction: nodeInteraction, transition: $0) })
|
||||
|> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue)
|
||||
}
|
||||
|
||||
let appliedTransition = callListNodeViewTransition |> deliverOnMainQueue |> mapToQueue { [weak self] transition -> Signal<Void, NoError> in
|
||||
if let strongSelf = self {
|
||||
return strongSelf.enqueueTransition(transition)
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
|
||||
self.listNode.displayedItemRangeChanged = { [weak self] range, transactionOpaqueState in
|
||||
if let strongSelf = self, let range = range.loadedRange, let view = (transactionOpaqueState as? CallListOpaqueTransactionState)?.callListView.originalView {
|
||||
var location: CallListNodeLocation?
|
||||
if range.firstIndex < 5 && view.later != nil {
|
||||
location = .navigation(index: view.entries[view.entries.count - 1].highestIndex)
|
||||
} else if range.firstIndex >= 5 && range.lastIndex >= view.entries.count - 5 && view.earlier != nil {
|
||||
location = .navigation(index: view.entries[0].lowestIndex)
|
||||
}
|
||||
|
||||
if let location = location, location != strongSelf.currentLocationAndType.location {
|
||||
strongSelf.currentLocationAndType = CallListNodeLocationAndType(location: location, type: strongSelf.currentLocationAndType.type)
|
||||
strongSelf.callListLocationAndType.set(strongSelf.currentLocationAndType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.callListDisposable.set(appliedTransition.start())
|
||||
|
||||
self.callListLocationAndType.set(self.currentLocationAndType)
|
||||
|
||||
let emptySignal = self.emptyStatePromise.get() |> distinctUntilChanged
|
||||
let typeSignal = self.callListLocationAndType.get() |> map { locationAndType -> CallListViewType in
|
||||
return locationAndType.type
|
||||
} |> distinctUntilChanged
|
||||
|
||||
self.emptyStateDisposable.set((combineLatest(emptySignal, typeSignal, self.statePromise.get()) |> deliverOnMainQueue).start(next: { [weak self] isEmpty, type, state in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateEmptyPlaceholder(theme: state.theme, strings: state.strings, type: type, hidden: !isEmpty)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.callListDisposable.dispose()
|
||||
self.emptyStateDisposable.dispose()
|
||||
}
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, disableAnimations: Bool) {
|
||||
if theme !== self.currentState.theme || strings !== self.currentState.strings || disableAnimations != self.currentState.disableAnimations {
|
||||
switch self.mode {
|
||||
case .tab:
|
||||
self.backgroundColor = theme.chatList.backgroundColor
|
||||
self.listNode.backgroundColor = theme.chatList.backgroundColor
|
||||
case .navigation:
|
||||
self.backgroundColor = theme.list.blocksBackgroundColor
|
||||
self.listNode.backgroundColor = theme.list.blocksBackgroundColor
|
||||
}
|
||||
|
||||
self.updateEmptyPlaceholder(theme: theme, strings: strings, type: self.currentLocationAndType.type, hidden: self.emptyTextNode.isHidden)
|
||||
|
||||
self.updateState {
|
||||
return $0.withUpdatedPresentationData(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, disableAnimations: disableAnimations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let textFont = Font.regular(16.0)
|
||||
|
||||
func updateEmptyPlaceholder(theme: PresentationTheme, strings: PresentationStrings, type: CallListViewType, hidden: Bool) {
|
||||
let alpha: CGFloat = hidden ? 0.0 : 1.0
|
||||
let previousAlpha = self.emptyTextNode.alpha
|
||||
self.emptyTextNode.alpha = alpha
|
||||
self.emptyTextNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.2)
|
||||
|
||||
if !hidden {
|
||||
let type = self.currentLocationAndType.type
|
||||
let string: String
|
||||
if type == .missed {
|
||||
string = strings.Calls_NoMissedCallsPlacehoder
|
||||
} else {
|
||||
string = strings.Calls_NoCallsPlaceholder
|
||||
}
|
||||
let color: UIColor
|
||||
|
||||
switch self.mode {
|
||||
case .tab:
|
||||
self.backgroundColor = theme.chatList.backgroundColor
|
||||
self.listNode.backgroundColor = theme.chatList.backgroundColor
|
||||
color = theme.list.freeTextColor
|
||||
case .navigation:
|
||||
self.backgroundColor = theme.list.blocksBackgroundColor
|
||||
self.listNode.backgroundColor = theme.list.blocksBackgroundColor
|
||||
color = theme.list.freeTextColor
|
||||
}
|
||||
|
||||
self.emptyTextNode.attributedText = NSAttributedString(string: string, font: textFont, textColor: color, paragraphAlignment: .center)
|
||||
if let layout = self.containerLayout {
|
||||
self.updateLayout(layout.0, navigationBarHeight: layout.1, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateState(_ f: (CallListNodeState) -> CallListNodeState) {
|
||||
let state = f(self.currentState)
|
||||
if state != self.currentState {
|
||||
self.currentState = state
|
||||
self.statePromise.set(state)
|
||||
}
|
||||
}
|
||||
|
||||
func updateType(_ type: CallListViewType) {
|
||||
if type != self.currentLocationAndType.type {
|
||||
if let view = self.callListView?.originalView {
|
||||
var index: MessageIndex
|
||||
if !view.entries.isEmpty {
|
||||
index = view.entries[view.entries.count - 1].highestIndex
|
||||
} else {
|
||||
index = MessageIndex.absoluteUpperBound()
|
||||
}
|
||||
self.currentLocationAndType = CallListNodeLocationAndType(location: .changeType(index: index), type: type)
|
||||
self.emptyStatePromise.set(.single(false))
|
||||
self.callListLocationAndType.set(self.currentLocationAndType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func enqueueTransition(_ transition: CallListNodeListViewTransition) -> Signal<Void, NoError> {
|
||||
return Signal { [weak self] subscriber in
|
||||
if let strongSelf = self {
|
||||
if let _ = strongSelf.enqueuedTransition {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
strongSelf.enqueuedTransition = (transition, {
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
|
||||
if strongSelf.isNodeLoaded {
|
||||
strongSelf.dequeueTransition()
|
||||
} else {
|
||||
if !strongSelf.didSetReady {
|
||||
strongSelf.didSetReady = true
|
||||
strongSelf._ready.set(true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
|
||||
return EmptyDisposable
|
||||
} |> runOn(Queue.mainQueue())
|
||||
}
|
||||
|
||||
private func dequeueTransition() {
|
||||
if let (transition, completion) = self.enqueuedTransition {
|
||||
self.enqueuedTransition = nil
|
||||
|
||||
let completion: (ListViewDisplayedItemRange) -> Void = { [weak self] visibleRange in
|
||||
if let strongSelf = self {
|
||||
strongSelf.callListView = transition.callListView
|
||||
|
||||
let empty = countMeaningfulCallListEntries(transition.callListView.filteredEntries) == 0
|
||||
strongSelf.emptyStateUpdated(empty)
|
||||
strongSelf.emptyStatePromise.set(.single(empty))
|
||||
|
||||
if !strongSelf.didSetReady {
|
||||
strongSelf.didSetReady = true
|
||||
strongSelf._ready.set(true)
|
||||
}
|
||||
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
self.listNode.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: CallListOpaqueTransactionState(callListView: transition.callListView), completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func scrollToLatest() {
|
||||
if let view = self.callListView?.originalView, view.later == nil {
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
} else {
|
||||
let location: CallListNodeLocation = .scroll(index: MessageIndex.absoluteUpperBound(), sourceIndex: MessageIndex.absoluteLowerBound(), scrollPosition: .top(0.0), animated: true)
|
||||
self.currentLocationAndType = CallListNodeLocationAndType(location: location, type: self.currentLocationAndType.type)
|
||||
self.callListLocationAndType.set(self.currentLocationAndType)
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
var insets = layout.insets(options: [.input])
|
||||
insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top)
|
||||
insets.left += layout.safeInsets.left
|
||||
insets.right += layout.safeInsets.right
|
||||
if self.mode == .navigation {
|
||||
insets.top += 64.0
|
||||
}
|
||||
|
||||
let size = layout.size
|
||||
let contentRect = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom))
|
||||
|
||||
|
||||
let textSize = self.emptyTextNode.measure(CGSize(width: size.width - 20.0, height: size.height))
|
||||
transition.updateFrame(node: self.emptyTextNode, frame: CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - textSize.width) / 2.0), y: contentRect.minY + floor((contentRect.height - textSize.height) / 2.0)), size: textSize))
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.containerLayout = (layout, navigationBarHeight)
|
||||
|
||||
var insets = layout.insets(options: [.input])
|
||||
insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top)
|
||||
insets.left += layout.safeInsets.left
|
||||
insets.right += layout.safeInsets.right
|
||||
|
||||
self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
|
||||
self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
|
||||
|
||||
updateLayout(layout, navigationBarHeight: navigationBarHeight, transition: transition)
|
||||
|
||||
var duration: Double = 0.0
|
||||
var curve: UInt = 0
|
||||
switch transition {
|
||||
case .immediate:
|
||||
break
|
||||
case let .animated(animationDuration, animationCurve):
|
||||
duration = animationDuration
|
||||
switch animationCurve {
|
||||
case .easeInOut, .custom:
|
||||
break
|
||||
case .spring:
|
||||
curve = 7
|
||||
}
|
||||
}
|
||||
|
||||
let listViewCurve: ListViewAnimationCurve
|
||||
if curve == 7 {
|
||||
listViewCurve = .Spring(duration: duration)
|
||||
} else {
|
||||
listViewCurve = .Default(duration: duration)
|
||||
}
|
||||
|
||||
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve)
|
||||
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
if !self.dequeuedInitialTransitionOnLayout {
|
||||
self.dequeuedInitialTransitionOnLayout = true
|
||||
self.dequeueTransition()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user