Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,421 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import ContactsPeerItem
import SearchUI
import SolidRoundedButtonNode
import PremiumUI
func localizedOldChannelDate(peer: InactiveChannel, strings: PresentationStrings) -> String {
let timestamp = peer.lastActivityDate
let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
var t: time_t = time_t(TimeInterval(timestamp))
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
var now: time_t = time_t(nowTimestamp)
var timeinfoNow: tm = tm()
localtime_r(&now, &timeinfoNow)
var string: String
if timeinfoNow.tm_year == timeinfo.tm_year && timeinfoNow.tm_mon == timeinfo.tm_mon {
//weeks
let dif = Int(roundf(Float(timeinfoNow.tm_mday - timeinfo.tm_mday) / 7))
string = strings.OldChannels_InactiveWeek(Int32(dif))
} else if timeinfoNow.tm_year == timeinfo.tm_year {
//month
let dif = Int(timeinfoNow.tm_mon - timeinfo.tm_mon)
string = strings.OldChannels_InactiveMonth(Int32(dif))
} else {
//year
var dif = Int(timeinfoNow.tm_year - timeinfo.tm_year)
if Int(timeinfoNow.tm_mon - timeinfo.tm_mon) > 6 {
dif += 1
}
string = strings.OldChannels_InactiveYear(Int32(dif))
}
if let channel = peer.peer as? TelegramChannel, case .group = channel.info {
if let participantsCount = peer.participantsCount, participantsCount != 0 {
string = strings.OldChannels_GroupFormat(participantsCount) + ", " + string
} else {
string = strings.OldChannels_GroupEmptyFormat + ", " + string
}
} else {
string = strings.OldChannels_ChannelFormat + string
}
return string
}
private final class OldChannelsItemArguments {
let context: AccountContext
let togglePeer: (EnginePeer.Id, Bool) -> Void
init(
context: AccountContext,
togglePeer: @escaping (EnginePeer.Id, Bool) -> Void
) {
self.context = context
self.togglePeer = togglePeer
}
}
private enum OldChannelsSection: Int32 {
case info
case peers
}
private enum OldChannelsEntryId: Hashable {
case info
case peersHeader
case peer(EnginePeer.Id)
}
private enum OldChannelsEntry: ItemListNodeEntry {
case info(Int32, Int32, Int32, String, Bool)
case peersHeader(String)
case peer(Int, InactiveChannel, Bool)
var section: ItemListSectionId {
switch self {
case .info:
return OldChannelsSection.info.rawValue
case .peersHeader, .peer:
return OldChannelsSection.peers.rawValue
}
}
var stableId: OldChannelsEntryId {
switch self {
case .info:
return .info
case .peersHeader:
return .peersHeader
case let .peer(_, peer, _):
return .peer(peer.peer.id)
}
}
static func ==(lhs: OldChannelsEntry, rhs: OldChannelsEntry) -> Bool {
switch lhs {
case let .info(count, limit, premiumLimit, text, isPremiumDisabled):
if case .info(count, limit, premiumLimit, text, isPremiumDisabled) = rhs {
return true
} else {
return false
}
case let .peersHeader(title):
if case .peersHeader(title) = rhs {
return true
} else {
return false
}
case let .peer(lhsIndex, lhsPeer, lhsSelected):
if case let .peer(rhsIndex, rhsPeer, rhsSelected) = rhs {
if lhsIndex != rhsIndex {
return false
}
if lhsPeer != rhsPeer {
return false
}
if lhsSelected != rhsSelected {
return false
}
return true
} else {
return false
}
}
}
static func <(lhs: OldChannelsEntry, rhs: OldChannelsEntry) -> Bool {
switch lhs {
case .info:
if case .info = rhs {
return false
} else {
return true
}
case .peersHeader:
switch rhs {
case .info, .peersHeader:
return false
case .peer:
return true
}
case let .peer(lhsIndex, _, _):
switch rhs {
case .info, .peersHeader:
return false
case let .peer(rhsIndex, _, _):
return lhsIndex < rhsIndex
}
}
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! OldChannelsItemArguments
switch self {
case let .info(count, limit, premiumLimit, text, isPremiumDisabled):
return IncreaseLimitHeaderItem(theme: presentationData.theme, strings: presentationData.strings, icon: .group, count: count, limit: limit, premiumCount: premiumLimit, text: text, isPremiumDisabled: isPremiumDisabled, sectionId: self.section)
case let .peersHeader(title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .peer(_, peer, selected):
return ContactsPeerItem(presentationData: presentationData, style: .blocks, systemStyle: .glass, sectionId: self.section, sortOrder: .firstLast, displayOrder: .firstLast, context: arguments.context, peerMode: .peer, peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .custom(string: NSAttributedString(string: localizedOldChannelDate(peer: peer, strings: presentationData.strings)), multiline: false, isActive: false, icon: nil), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in
arguments.togglePeer(peer.peer.id, true)
}, setPeerIdWithRevealedOptions: nil, deletePeer: nil, itemHighlighting: nil, contextAction: nil)
}
}
}
private struct OldChannelsState: Equatable {
var selectedPeers: Set<EnginePeer.Id> = Set()
var isSearching: Bool = false
}
private func oldChannelsEntries(presentationData: PresentationData, state: OldChannelsState, isPremium: Bool, isPremiumDisabled: Bool, limit: Int32, premiumLimit: Int32, peers: [InactiveChannel]?, intent: OldChannelsControllerIntent) -> [OldChannelsEntry] {
var entries: [OldChannelsEntry] = []
let count = max(isPremium ? premiumLimit : limit, Int32(peers?.count ?? 0))
var text: String?
if count >= premiumLimit {
switch intent {
case .create:
text = presentationData.strings.OldChannels_TooManyCommunitiesCreateFinalText("\(premiumLimit)").string
case .upgrade:
text = presentationData.strings.OldChannels_TooManyCommunitiesUpgradeFinalText("\(premiumLimit)").string
case .join:
text = presentationData.strings.OldChannels_TooManyCommunitiesFinalText("\(premiumLimit)").string
}
} else if count >= limit {
if isPremiumDisabled {
switch intent {
case .create:
text = presentationData.strings.OldChannels_TooManyCommunitiesCreateNoPremiumText("\(premiumLimit)").string
case .upgrade:
text = presentationData.strings.OldChannels_TooManyCommunitiesUpgradeNoPremiumText("\(premiumLimit)").string
case .join:
text = presentationData.strings.OldChannels_TooManyCommunitiesNoPremiumText("\(count)").string
}
} else {
switch intent {
case .create:
text = presentationData.strings.OldChannels_TooManyCommunitiesCreateText("\(count)", "\(premiumLimit)").string
case .upgrade:
text = presentationData.strings.OldChannels_TooManyCommunitiesUpgradeText("\(count)", "\(premiumLimit)").string
case .join:
text = presentationData.strings.OldChannels_TooManyCommunitiesText("\(count)", "\(premiumLimit)").string
}
}
}
if let text = text {
entries.append(.info(count, limit, premiumLimit, text, isPremiumDisabled))
}
if let peers = peers, !peers.isEmpty {
entries.append(.peersHeader(presentationData.strings.OldChannels_ChannelsHeader))
for peer in peers {
entries.append(.peer(entries.count, peer, state.selectedPeers.contains(peer.peer.id)))
}
}
return entries
}
public func oldChannelsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, intent: OldChannelsControllerIntent, completed: @escaping (Bool) -> Void = { _ in }) -> ViewController {
let initialState = OldChannelsState()
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((OldChannelsState) -> OldChannelsState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var dismissImpl: (() -> Void)?
var pushImpl: ((ViewController) -> Void)?
var setDisplayNavigationBarImpl: ((Bool) -> Void)?
var ensurePeerVisibleImpl: ((EnginePeer.Id) -> Void)?
var leaveActionImpl: (() -> Void)?
let actionsDisposable = DisposableSet()
let arguments = OldChannelsItemArguments(
context: context,
togglePeer: { peerId, ensureVisible in
var didSelect = false
updateState { state in
var state = state
if state.selectedPeers.contains(peerId) {
state.selectedPeers.remove(peerId)
} else {
state.selectedPeers.insert(peerId)
didSelect = true
}
return state
}
if didSelect && ensureVisible {
ensurePeerVisibleImpl?(peerId)
}
}
)
let selectedPeerIds = statePromise.get()
|> map { $0.selectedPeers }
|> distinctUntilChanged
let peersSignal: Signal<[InactiveChannel]?, NoError> = .single(nil)
|> then(
context.engine.peers.inactiveChannelList()
|> map { peers -> [InactiveChannel]? in
return peers.sorted(by: { lhs, rhs in
return lhs.lastActivityDate < rhs.lastActivityDate
})
}
)
let peersPromise = Promise<[InactiveChannel]?>()
peersPromise.set(peersSignal)
var previousPeersWereEmpty = true
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let signal = combineLatest(
queue: Queue.mainQueue(),
presentationData,
statePromise.get(),
peersPromise.get(),
context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId),
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)
)
)
|> map { presentationData, state, peers, limits -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
})
let (accountPeer, limits, premiumLimits) = limits
let isPremium = accountPeer?.isPremium ?? false
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.OldChannels_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
var searchItem: OldChannelsSearchItem?
searchItem = OldChannelsSearchItem(context: context, theme: presentationData.theme, placeholder: presentationData.strings.Common_Search, activated: state.isSearching, updateActivated: { value in
if !value {
setDisplayNavigationBarImpl?(true)
}
updateState { state in
var state = state
state.isSearching = value
return state
}
if value {
setDisplayNavigationBarImpl?(false)
}
}, peers: peersPromise.get() |> map { $0 ?? [] }, selectedPeerIds: selectedPeerIds, togglePeer: { peerId in
arguments.togglePeer(peerId, false)
})
let peersAreEmpty = peers == nil
let peersAreEmptyUpdated = previousPeersWereEmpty != peersAreEmpty
previousPeersWereEmpty = peersAreEmpty
var emptyStateItem: ItemListControllerEmptyStateItem?
if peersAreEmpty {
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
}
let buttonText: String
let colorful: Bool
if state.selectedPeers.count > 0 {
buttonText = presentationData.strings.OldChannels_LeaveCommunities(Int32(state.selectedPeers.count))
colorful = false
} else {
buttonText = presentationData.strings.Premium_IncreaseLimit
colorful = true
}
let footerItem: IncreaseLimitFooterItem?
if (state.isSearching || premiumConfiguration.isPremiumDisabled || isPremium) && state.selectedPeers.count == 0 {
footerItem = nil
} else {
footerItem = IncreaseLimitFooterItem(theme: presentationData.theme, title: buttonText, colorful: colorful, action: {
if state.selectedPeers.count > 0 {
leaveActionImpl?()
} else {
let controller = PremiumIntroScreen(context: context, source: .groupsAndChannels)
pushImpl?(controller)
}
})
}
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: oldChannelsEntries(presentationData: presentationData, state: state, isPremium: isPremium, isPremiumDisabled: premiumConfiguration.isPremiumDisabled, limit: limits.maxChannelsCount, premiumLimit: premiumLimits.maxChannelsCount, peers: peers, intent: intent), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, footerItem: footerItem, initialScrollToItem: ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up), crossfadeState: peersAreEmptyUpdated, animateChanges: false)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
controller.navigationPresentation = .modal
leaveActionImpl = {
let state = stateValue.with { $0 }
let _ = (peersPromise.get()
|> take(1)
|> mapToSignal { peers -> Signal<Never, NoError> in
let peers = peers ?? []
let ensureStoredPeers = peers.map { $0.peer }.filter { state.selectedPeers.contains($0.id) }
let ensureStoredPeersSignal: Signal<Never, NoError> = context.engine.peers.ensurePeersAreLocallyAvailable(peers: ensureStoredPeers.map(EnginePeer.init))
return ensureStoredPeersSignal
|> then(context.engine.peers.removePeerChats(peerIds: Array(ensureStoredPeers.map(\.id))))
}
|> deliverOnMainQueue).start(completed: {
completed(true)
dismissImpl?()
})
}
dismissImpl = { [weak controller] in
controller?.dismiss()
}
pushImpl = { [weak controller] c in
controller?.push(c)
}
setDisplayNavigationBarImpl = { [weak controller] display in
controller?.setDisplayNavigationBar(display, transition: .animated(duration: 0.5, curve: .spring))
}
ensurePeerVisibleImpl = { [weak controller] peerId in
guard let controller = controller else {
return
}
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ContactsPeerItemNode, let peer = itemNode.chatPeer, peer.id == peerId {
controller.ensureItemNodeVisible(itemNode, curve: .Spring(duration: 0.3))
}
}
}
return controller
}
@@ -0,0 +1,442 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import MergeLists
import ItemListUI
import PresentationDataUtils
import AccountContext
import SearchBarNode
import SearchUI
import ChatListSearchItemHeader
import ContactsPeerItem
//Xcode 16
#if SWIFT_PACKAGE
extension NavigationBarSearchContentNode: ItemListControllerSearchNavigationContentNode {
public func activate() {
}
public func deactivate() {
}
public func setQueryUpdated(_ f: @escaping (String) -> Void) {
}
}
#else
extension NavigationBarSearchContentNode: @retroactive ItemListControllerSearchNavigationContentNode {
public func activate() {
}
public func deactivate() {
}
public func setQueryUpdated(_ f: @escaping (String) -> Void) {
}
}
#endif
final class OldChannelsSearchItem: ItemListControllerSearch {
let context: AccountContext
let theme: PresentationTheme
let placeholder: String
let activated: Bool
let updateActivated: (Bool) -> Void
let peers: Signal<[InactiveChannel], NoError>
let selectedPeerIds: Signal<Set<EnginePeer.Id>, NoError>
let togglePeer: (EnginePeer.Id) -> Void
private var updateActivity: ((Bool) -> Void)?
private var activity: ValuePromise<Bool> = ValuePromise(ignoreRepeated: false)
private let activityDisposable = MetaDisposable()
init(context: AccountContext, theme: PresentationTheme, placeholder: String, activated: Bool, updateActivated: @escaping (Bool) -> Void, peers: Signal<[InactiveChannel], NoError>, selectedPeerIds: Signal<Set<EnginePeer.Id>, NoError>, togglePeer: @escaping (EnginePeer.Id) -> Void) {
self.context = context
self.theme = theme
self.placeholder = placeholder
self.activated = activated
self.updateActivated = updateActivated
self.peers = peers
self.selectedPeerIds = selectedPeerIds
self.togglePeer = togglePeer
}
deinit {
self.activityDisposable.dispose()
}
func isEqual(to: ItemListControllerSearch) -> Bool {
if let to = to as? OldChannelsSearchItem {
if self.context !== to.context || self.theme !== to.theme || self.placeholder != to.placeholder || self.activated != to.activated {
return false
}
return true
} else {
return false
}
}
func titleContentNode(current: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)? {
let updateActivated: (Bool) -> Void = self.updateActivated
if let current = current as? NavigationBarSearchContentNode {
current.updateThemeAndPlaceholder(theme: self.theme, placeholder: self.placeholder)
return current
} else {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
return NavigationBarSearchContentNode(theme: presentationData.theme, placeholder: presentationData.strings.Settings_Search, activate: {
updateActivated(true)
})
}
}
func node(current: ItemListControllerSearchNode?, titleContentNode: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> ItemListControllerSearchNode {
let updateActivated: (Bool) -> Void = self.updateActivated
if let current = current as? OldChannelsSearchItemNode, let titleContentNode = titleContentNode as? NavigationBarSearchContentNode {
current.updatePresentationData(self.context.sharedContext.currentPresentationData.with { $0 })
if current.isSearching != self.activated {
if self.activated {
current.activateSearch(placeholderNode: titleContentNode.placeholderNode)
} else {
current.deactivateSearch(placeholderNode: titleContentNode.placeholderNode)
}
}
return current
} else {
return OldChannelsSearchItemNode(context: self.context, cancel: {
updateActivated(false)
}, peers: self.peers, selectedPeerIds: self.selectedPeerIds, togglePeer: self.togglePeer)
}
}
}
private final class OldChannelsSearchInteraction {
let togglePeer: (EnginePeer.Id) -> Void
init(togglePeer: @escaping (EnginePeer.Id) -> Void) {
self.togglePeer = togglePeer
}
}
private enum OldChannelsSearchEntry: Comparable, Identifiable {
case peer(Int, InactiveChannel, Bool)
var stableId: EnginePeer.Id {
switch self {
case let .peer(_, peer, _):
return peer.peer.id
}
}
private func index() -> Int {
switch self {
case let .peer(index, _, _):
return index
}
}
static func <(lhs: OldChannelsSearchEntry, rhs: OldChannelsSearchEntry) -> Bool {
return lhs.index() < rhs.index()
}
static func ==(lhs: OldChannelsSearchEntry, rhs: OldChannelsSearchEntry) -> Bool {
if case let .peer(index, peer, isSelected) = lhs {
if case .peer(index, peer, isSelected) = rhs {
return true
}
}
return false
}
func item(context: AccountContext, presentationData: ItemListPresentationData, interaction: OldChannelsSearchInteraction) -> ListViewItem {
switch self {
case let .peer(_, peer, selected):
return ContactsPeerItem(presentationData: presentationData, style: .plain, sortOrder: .firstLast, displayOrder: .firstLast, context: context, peerMode: .peer, peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .custom(string: NSAttributedString(string: localizedOldChannelDate(peer: peer, strings: presentationData.strings)), multiline: false, isActive: false, icon: nil), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in
interaction.togglePeer(peer.peer.id)
}, setPeerIdWithRevealedOptions: nil, deletePeer: nil, itemHighlighting: nil, contextAction: nil)
}
}
}
private struct OldChannelsSearchContainerTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let isSearching: Bool
}
private func preparedOldChannelsSearchContainerTransition(presentationData: ItemListPresentationData, from fromEntries: [OldChannelsSearchEntry], to toEntries: [OldChannelsSearchEntry], context: AccountContext, interaction: OldChannelsSearchInteraction, isSearching: Bool, forceUpdate: Bool) -> OldChannelsSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) }
return OldChannelsSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching)
}
private final class OldChannelsSearchContainerNode: SearchDisplayControllerContentNode {
private let listNode: ListView
private var enqueuedTransitions: [OldChannelsSearchContainerTransition] = []
private var hasValidLayout = false
private let searchQuery = Promise<String?>()
private let searchDisposable = MetaDisposable()
private var recentDisposable: Disposable?
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let presentationDataPromise: Promise<PresentationData>
init(context: AccountContext, peers: Signal<[InactiveChannel], NoError>, selectedPeerIds: Signal<Set<EnginePeer.Id>, NoError>, togglePeer: @escaping (EnginePeer.Id) -> Void) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData
self.presentationDataPromise = Promise(self.presentationData)
self.listNode = ListView()
self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.listNode.isHidden = true
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
super.init()
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.addSubnode(self.listNode)
let interaction = OldChannelsSearchInteraction(togglePeer: { [weak self] peerId in
togglePeer(peerId)
if let strongSelf = self {
strongSelf.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ContactsPeerItemNode, let peer = itemNode.chatPeer, peer.id == peerId {
strongSelf.listNode.ensureItemNodeVisible(itemNode, curve: .Spring(duration: 0.3))
}
}
}
})
let queryAndFoundItems: Signal<(String, [OldChannelsSearchEntry])?, NoError> = combineLatest(self.searchQuery.get(), peers, selectedPeerIds)
|> mapToSignal { query, peers, selectedPeerIds -> Signal<(String, [OldChannelsSearchEntry])?, NoError> in
if let query = query, !query.isEmpty {
var results: [OldChannelsSearchEntry] = []
let normalizedQuery = query.lowercased()
for peer in peers {
if peer.peer.indexName.matchesByTokens(normalizedQuery) {
results.append(.peer(results.count, peer, selectedPeerIds.contains(peer.peer.id)))
}
}
return .single((query, results))
} else {
return .single(nil)
}
}
let previousEntriesHolder = Atomic<([OldChannelsSearchEntry], PresentationTheme, PresentationStrings)?>(value: nil)
self.searchDisposable.set(combineLatest(queue: .mainQueue(), queryAndFoundItems, self.presentationDataPromise.get()).start(next: { [weak self] queryAndFoundItems, presentationData in
guard let strongSelf = self else {
return
}
var currentQuery: String?
var entries: [OldChannelsSearchEntry] = []
if let (query, items) = queryAndFoundItems {
currentQuery = query
for item in items {
entries.append(item)
}
}
if !entries.isEmpty || currentQuery == nil {
let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings))
let transition = preparedOldChannelsSearchContainerTransition(presentationData: ItemListPresentationData(presentationData), from: previousEntriesAndPresentationData?.0 ?? [], to: entries, context: context, interaction: interaction, isSearching: queryAndFoundItems != nil, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings)
strongSelf.enqueueTransition(transition)
}
}))
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
strongSelf.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
strongSelf.presentationDataPromise.set(.single(presentationData))
}
}
})
self.listNode.beganInteractiveDragging = { [weak self] _ in
self?.dismissInput?()
}
}
deinit {
self.searchDisposable.dispose()
self.recentDisposable?.dispose()
self.presentationDataDisposable?.dispose()
}
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.listNode.backgroundColor = theme.chatList.backgroundColor
}
override func searchTextUpdated(text: String) {
if text.isEmpty {
self.searchQuery.set(.single(nil))
} else {
self.searchQuery.set(.single(text))
}
}
private func enqueueTransition(_ transition: OldChannelsSearchContainerTransition) {
self.enqueuedTransitions.append(transition)
if self.hasValidLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func dequeueTransition() {
if let transition = self.enqueuedTransitions.first {
self.enqueuedTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
options.insert(.Synchronous)
options.insert(.PreferSynchronousDrawing)
options.insert(.PreferSynchronousResourceLoading)
let isSearching = transition.isSearching
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
self?.listNode.isHidden = !isSearching
})
}
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
var insets = layout.insets(options: [.input])
insets.top += navigationBarHeight
insets.left += layout.safeInsets.left
insets.right += layout.safeInsets.right
self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
if !self.hasValidLayout {
self.hasValidLayout = true
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
override func scrollToTop() {
let listNodeToScroll: ListView = self.listNode
listNodeToScroll.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.cancel?()
}
}
}
private final class OldChannelsSearchItemNode: ItemListControllerSearchNode {
private let context: AccountContext
private var presentationData: PresentationData
private var containerLayout: (ContainerViewLayout, CGFloat)?
private var searchDisplayController: SearchDisplayController?
var cancel: () -> Void
private let peers: Signal<[InactiveChannel], NoError>
private let selectedPeerIds: Signal<Set<EnginePeer.Id>, NoError>
private let togglePeer: (EnginePeer.Id) -> Void
init(context: AccountContext, cancel: @escaping () -> Void, peers: Signal<[InactiveChannel], NoError>, selectedPeerIds: Signal<Set<EnginePeer.Id>, NoError>, togglePeer: @escaping (EnginePeer.Id) -> Void) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.cancel = cancel
self.peers = peers
self.selectedPeerIds = selectedPeerIds
self.togglePeer = togglePeer
super.init()
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.searchDisplayController?.updatePresentationData(presentationData)
}
func activateSearch(placeholderNode: SearchBarPlaceholderNode) {
guard let (containerLayout, navigationBarHeight) = self.containerLayout, self.searchDisplayController == nil else {
return
}
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: OldChannelsSearchContainerNode(context: self.context, peers: self.peers, selectedPeerIds: self.selectedPeerIds, togglePeer: self.togglePeer), cancel: { [weak self] in
self?.cancel()
})
self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate)
self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in
if let strongSelf = self, let strongPlaceholderNode = placeholderNode {
if isSearchBar {
strongPlaceholderNode.supernode?.insertSubnode(subnode, aboveSubnode: strongPlaceholderNode)
} else {
strongSelf.addSubnode(subnode)
}
}
}, placeholder: placeholderNode)
}
func deactivateSearch(placeholderNode: SearchBarPlaceholderNode) {
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.deactivate(placeholder: placeholderNode)
self.searchDisplayController = nil
}
}
var isSearching: Bool {
return self.searchDisplayController != nil
}
override func scrollToTop() {
self.searchDisplayController?.contentNode.scrollToTop()
}
override func queryUpdated(_ query: String) {
}
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.containerLayout = (layout, navigationBarHeight)
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let searchDisplayController = self.searchDisplayController, let result = searchDisplayController.contentNode.hitTest(self.view.convert(point, to: searchDisplayController.contentNode.view), with: event) {
return result
}
return super.hitTest(point, with: event)
}
}