Files
Telegram-iOS/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift
T
isaacisaacClaude Opus 4.7
d1aa0db537 Postbox -> TelegramEngine waves 46-93 (squashed)
Squash of 63 commits spanning waves 46-93 (plus interspersed docs commits)
of the gradual Postbox->TelegramEngine consumer-side migration.

Scope: 139 files changed, 2123 insertions(+), 452 deletions(-).

## Themes by wave-block

**Waves 46-58 — Peer field migrations + facade additions**
Foundational EnginePeer convenience init additions (PeerReference, RenderedPeer,
SelectivePrivacyPeer). Multiple `peer: Peer` field migrations across PeerInfo,
ChatList, and SettingsUI components.

**Waves 59-73 — peer field cascade + EnginePeer wrap drops**
Series of single- to two-file peer-field migrations; consumer-side wrap
removal (`EnginePeer(peer)` -> direct EnginePeer use); `as? TelegramUser`
cast conversion to `case let .user(...)` enum match. Wave 64: RenderedPeer
convenience init. Wave 68: SelectivePrivacyPeer convenience init.

**Waves 74-83 — controller-Node bridge cleanup + small migrations**
Wave-71 shadow-pattern cleanup at controller->Node bridges. Migrations of
ChatRecentActionsController.peer (74), PeerInfoMember (75), MentionChatInputPanelItem
(76), PassportUI SecureIdAuthController (77), AccountWithInfo + ShareController
(78), peerInputActivitiesPromise (79), InactiveChannel (80), BlockedPeers (81),
openHashtag resolveSignal (82), NotificationExceptionsList (83).

**Waves 84-90 — TelegramEngine.Resources facade migrations**
Per-method Shape-A/B sweeps converting `<ctx>.account.postbox.mediaBox.X(...)`
to `<ctx>.engine.resources.X(...)`. Wave 90 was a single-commit big sweep:
40 fetchedMediaResource sites in 25 files migrated to engine.resources.fetch
facade in one atomic pass with first-pass-clean build.

Methods covered: storeResourceData, completedResourcePath, cancelInteractiveResourceFetch,
resourceRangesStatus, resourceStatus, fetch (fetchedMediaResource).

**Waves 91-92 — additional type migrations**
Wave 91: ItemListWebsiteItem.peer + RecentSessionsController enum-case payload
+ openWebSession callback Peer? -> EnginePeer?.
Wave 92: ChatListController StateHolder.EntryContext status type
MediaResourceStatus -> EngineMediaResource.FetchStatus.

**Wave 93 — speculative `import Postbox` drop sweep**
Drop import from 7 wave-touched files where it became unused; restore in 5
files where bare PeerId/Message/MediaId/StoryId references escaped the
pre-flight regex. Includes one MediaId(...) -> EngineMedia.Id(...) swap in
InAppPurchaseManager to unlock its import drop.

## Build state

Final state at squash: clean Telegram/Telegram build at debug_sim_arm64.

## Persistent-state notes

- Pre-existing WIP unchanged across the squashed range:
  - build-system/bazel-rules/sourcekit-bazel-bsp submodule marker
  - Untracked: build-system/tulsi/, submodules/TgVoip/, third-party/libx264/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:48:15 +04:00

422 lines
17 KiB
Swift

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 case let .channel(channel) = peer.peer, 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: peer.peer, chatPeer: 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)
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
}