Swiftgram/submodules/CallListUI/Sources/CallListControllerNode.swift
2021-02-19 20:27:25 +04:00

735 lines
37 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SyncCore
import SwiftSignalKit
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
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, Bool) -> Void
let openInfo: (PeerId, [Message]) -> Void
let delete: ([MessageId]) -> Void
let updateShowCallsTab: (Bool) -> Void
let openGroupCall: (PeerId) -> Void
init(setMessageIdWithRevealedOptions: @escaping (MessageId?, MessageId?) -> Void, call: @escaping (PeerId, Bool) -> Void, openInfo: @escaping (PeerId, [Message]) -> Void, delete: @escaping ([MessageId]) -> Void, updateShowCallsTab: @escaping (Bool) -> Void, openGroupCall: @escaping (PeerId) -> Void) {
self.setMessageIdWithRevealedOptions = setMessageIdWithRevealedOptions
self.call = call
self.openInfo = openInfo
self.delete = delete
self.updateShowCallsTab = updateShowCallsTab
self.openGroupCall = openGroupCall
}
}
struct CallListNodeState: Equatable {
let presentationData: ItemListPresentationData
let dateTimeFormat: PresentationDateTimeFormat
let disableAnimations: Bool
let editing: Bool
let messageIdWithRevealedOptions: MessageId?
func withUpdatedPresentationData(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, disableAnimations: Bool) -> CallListNodeState {
return CallListNodeState(presentationData: presentationData, dateTimeFormat: dateTimeFormat, disableAnimations: disableAnimations, editing: self.editing, messageIdWithRevealedOptions: self.messageIdWithRevealedOptions)
}
func withUpdatedEditing(_ editing: Bool) -> CallListNodeState {
return CallListNodeState(presentationData: self.presentationData, dateTimeFormat: self.dateTimeFormat, disableAnimations: self.disableAnimations, editing: editing, messageIdWithRevealedOptions: self.messageIdWithRevealedOptions)
}
func withUpdatedMessageIdWithRevealedOptions(_ messageIdWithRevealedOptions: MessageId?) -> CallListNodeState {
return CallListNodeState(presentationData: self.presentationData, dateTimeFormat: self.dateTimeFormat, disableAnimations: self.disableAnimations, editing: self.editing, messageIdWithRevealedOptions: messageIdWithRevealedOptions)
}
static func ==(lhs: CallListNodeState, rhs: CallListNodeState) -> Bool {
if lhs.presentationData != rhs.presentationData {
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(context: AccountContext, presentationData: ItemListPresentationData, showSettings: Bool, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] {
return entries.map { entry -> ListViewInsertItem in
switch entry.entry {
case let .displayTab(_, text, value):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, noCorners: true, sectionId: 0, style: .blocks, updated: { value in
nodeInteraction.updateShowCallsTab(value)
}), directionHint: entry.directionHint)
case let .displayTabInfo(_, text):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
case let .groupCall(peer, _, isActive):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListGroupCallItem(presentationData: presentationData, context: context, style: showSettings ? .blocks : .plain, peer: peer, isActive: isActive, editing: false, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .messageEntry(topMessage, messages, _, _, dateTimeFormat, editing, hasActiveRevealControls, displayHeader, _):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, context: context, style: showSettings ? .blocks : .plain, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, displayHeader: displayHeader, 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(context: AccountContext, presentationData: ItemListPresentationData, showSettings: Bool, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] {
return entries.map { entry -> ListViewUpdateItem in
switch entry.entry {
case let .displayTab(_, text, value):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, noCorners: true, sectionId: 0, style: .blocks, updated: { value in
nodeInteraction.updateShowCallsTab(value)
}), directionHint: entry.directionHint)
case let .displayTabInfo(_, text):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
case let .groupCall(peer, _, isActive):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListGroupCallItem(presentationData: presentationData, context: context, style: showSettings ? .blocks : .plain, peer: peer, isActive: isActive, editing: false, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .messageEntry(topMessage, messages, _, _, dateTimeFormat, editing, hasActiveRevealControls, displayHeader, _):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, context: context, style: showSettings ? .blocks : .plain, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, displayHeader: displayHeader, 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(context: AccountContext, presentationData: ItemListPresentationData, showSettings: Bool, nodeInteraction: CallListNodeInteraction, transition: CallListNodeViewTransition) -> CallListNodeListViewTransition {
return CallListNodeListViewTransition(callListView: transition.callListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, presentationData: presentationData, showSettings: showSettings, nodeInteraction: nodeInteraction, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, presentationData: presentationData, 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 weak var controller: CallListController?
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 leftOverlayNode: ASDisplayNode
private let rightOverlayNode: ASDisplayNode
private let emptyTextNode: ASTextNode
private let call: (PeerId, Bool) -> Void
private let openInfo: (PeerId, [Message]) -> Void
private let emptyStateUpdated: (Bool) -> Void
private let emptyStatePromise = Promise<Bool>()
private let emptyStateDisposable = MetaDisposable()
private let openGroupCallDisposable = MetaDisposable()
init(controller: CallListController, context: AccountContext, mode: CallListControllerMode, presentationData: PresentationData, call: @escaping (PeerId, Bool) -> Void, openInfo: @escaping (PeerId, [Message]) -> Void, emptyStateUpdated: @escaping (Bool) -> Void) {
self.controller = controller
self.context = context
self.mode = mode
self.presentationData = presentationData
self.call = call
self.openInfo = openInfo
self.emptyStateUpdated = emptyStateUpdated
self.currentState = CallListNodeState(presentationData: ItemListPresentationData(presentationData), 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.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).0
}
self.leftOverlayNode = ASDisplayNode()
self.leftOverlayNode.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
self.rightOverlayNode = ASDisplayNode()
self.rightOverlayNode.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
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, isVideo in
self?.call(peerId, isVideo)
}, openInfo: { [weak self] peerId, messages in
self?.openInfo(peerId, messages)
}, delete: { [weak self] messageIds in
guard let strongSelf = self, let peerId = messageIds.first?.peerId else {
return
}
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peerId)
}
|> deliverOnMainQueue).start(next: { peer in
guard let strongSelf = self, let peer = peer else {
return
}
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
var items: [ActionSheetItem] = []
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_DeleteMessagesFor(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: messageIds, type: .forEveryone).start()
}))
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: messageIds, type: .forLocalPeer).start()
}))
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
strongSelf.controller?.present(actionSheet, in: .window(.root))
})
}, 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()
}
}
}, openGroupCall: { [weak self] peerId in
guard let strongSelf = self else {
return
}
let disposable = strongSelf.openGroupCallDisposable
let account = strongSelf.context.account
var signal: Signal<CachedChannelData.ActiveCall?, NoError> = strongSelf.context.account.postbox.transaction { transaction -> CachedChannelData.ActiveCall? in
return (transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData)?.activeCall
}
|> mapToSignal { activeCall -> Signal<CachedChannelData.ActiveCall?, NoError> in
if let activeCall = activeCall {
return .single(activeCall)
} else {
return updatedCurrentPeerGroupCall(account: account, peerId: peerId)
}
}
var cancelImpl: (() -> Void)?
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
if let strongSelf = self {
strongSelf.controller?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
signal = signal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
disposable.set(nil)
}
disposable.set((signal
|> deliverOnMainQueue).start(next: { activeCall in
guard let strongSelf = self else {
return
}
if let activeCall = activeCall {
strongSelf.context.joinGroupCall(peerId: peerId, activeCall: activeCall)
}
}))
})
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 = CallListSettings.defaultSettings.showTab
if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.callListSettings] as? CallListSettings {
value = settings.showTab
}
return value
}
let currentGroupCallPeerId: Signal<PeerId?, NoError>
if let callManager = context.sharedContext.callManager {
currentGroupCallPeerId = callManager.currentGroupCallSignal
|> map { call -> PeerId? in
call?.peerId
}
|> distinctUntilChanged
} else {
currentGroupCallPeerId = .single(nil)
}
let groupCalls = context.account.postbox.tailChatListView(groupId: .root, count: 100, summaryComponents: ChatListEntrySummaryComponents())
|> map { view -> [Peer] in
var result: [Peer] = []
for entry in view.0.entries {
switch entry {
case let .MessageEntry(_, _, _, _, _, renderedPeer, _, _, _, _):
if let channel = renderedPeer.peer as? TelegramChannel, channel.flags.contains(.hasActiveVoiceChat) {
result.append(channel)
}
default:
break
}
}
return result.sorted(by: { lhs, rhs in
let lhsTitle = lhs.compactDisplayTitle
let rhsTitle = rhs.compactDisplayTitle
if lhsTitle != rhsTitle {
return lhsTitle < rhsTitle
}
return lhs.id < rhs.id
})
}
|> distinctUntilChanged(isEqual: { lhs, rhs in
if lhs.count != rhs.count {
return false
}
for i in 0 ..< lhs.count {
if !lhs[i].isEqual(rhs[i]) {
return false
}
}
return true
})
let callListNodeViewTransition = combineLatest(callListViewUpdate, self.statePromise.get(), groupCalls, showCallsTab, currentGroupCallPeerId)
|> mapToQueue { (updateAndType, state, groupCalls, showCallsTab, currentGroupCallPeerId) -> Signal<CallListNodeListViewTransition, NoError> in
let (update, type) = updateAndType
let processedView = CallListNodeView(originalView: update.view, filteredEntries: callListNodeEntriesForView(view: update.view, groupCalls: groupCalls, state: state, showSettings: showSettings, showCallsTab: showCallsTab, isRecentCalls: type == .all, currentGroupCallPeerId: currentGroupCallPeerId), presentationData: state.presentationData)
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(context: context, presentationData: state.presentationData, 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.presentationData.theme, strings: state.presentationData.strings, type: type, hidden: !isEmpty)
}
}))
}
deinit {
self.callListDisposable.dispose()
self.emptyStateDisposable.dispose()
self.openGroupCallDisposable.dispose()
}
func updateThemeAndStrings(presentationData: PresentationData) {
if presentationData.theme !== self.currentState.presentationData.theme || presentationData.strings !== self.currentState.presentationData.strings || presentationData.disableAnimations != self.currentState.disableAnimations {
self.presentationData = presentationData
self.leftOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.rightOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
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
}
self.updateEmptyPlaceholder(theme: presentationData.theme, strings: presentationData.strings, type: self.currentLocationAndType.type, hidden: self.emptyTextNode.isHidden)
self.updateState {
return $0.withUpdatedPresentationData(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, disableAnimations: presentationData.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)
self.updateLayout(layout, navigationBarHeight: navigationBarHeight, transition: transition)
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve)
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()
}
}
}