Telegram-iOS/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoMembers.swift
isaac 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

381 lines
14 KiB
Swift

import Foundation
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import TemporaryCachedPeerDataManager
enum PeerInfoMemberRole {
case creator
case admin
case member
}
enum PeerInfoMember: Equatable {
case channelMember(participant: RenderedChannelParticipant, storyStats: PeerStoryStats?)
case legacyGroupMember(peer: RenderedPeer, role: PeerInfoMemberRole, invitedBy: PeerId?, presence: TelegramUserPresence?, storyStats: PeerStoryStats?, rank: String?)
case account(peer: RenderedPeer)
var id: PeerId {
switch self {
case let .channelMember(participant, _):
return participant.peer.id
case let .legacyGroupMember(peer, _, _, _, _, _):
return peer.peerId
case let .account(peer):
return peer.peerId
}
}
var peer: EnginePeer {
switch self {
case let .channelMember(participant, _):
return participant.peer
case let .legacyGroupMember(peer, _, _, _, _, _):
return EnginePeer(peer.peers[peer.peerId]!)
case let .account(peer):
return EnginePeer(peer.peers[peer.peerId]!)
}
}
var presence: TelegramUserPresence? {
switch self {
case let .channelMember(participant, _):
return participant.presences[participant.peer.id] as? TelegramUserPresence
case let .legacyGroupMember(_, _, _, presence, _, _):
return presence
case .account:
return nil
}
}
var role: PeerInfoMemberRole {
switch self {
case let .channelMember(participant, _):
switch participant.participant {
case .creator:
return .creator
case let .member(_, _, adminInfo, _, _, _):
if adminInfo != nil {
return .admin
} else {
return .member
}
}
case let .legacyGroupMember(_, role, _, _, _, _):
return role
case .account:
return .member
}
}
var rank: String? {
switch self {
case let .channelMember(participant, _):
switch participant.participant {
case let .creator(_, _, rank):
return rank
case let .member(_, _, _, _, rank, _):
return rank
}
case let .legacyGroupMember(_, _, _, _, _, rank):
return rank
case .account:
return nil
}
}
var storyStats: PeerStoryStats? {
switch self {
case let .channelMember(_, value):
return value
case let .legacyGroupMember(_, _, _, _, value, _):
return value
case .account:
return nil
}
}
}
enum PeerInfoMembersDataState: Equatable {
case loading(isInitial: Bool)
case ready(canLoadMore: Bool)
}
struct PeerInfoMembersState: Equatable {
var canAddMembers: Bool
var members: [PeerInfoMember]
var dataState: PeerInfoMembersDataState
}
private func membersSortedByPresence(_ members: [PeerInfoMember], accountPeerId: PeerId) -> [PeerInfoMember] {
return members.sorted(by: { lhs, rhs in
if lhs.id == accountPeerId {
return true
} else if rhs.id == accountPeerId {
return false
}
let lhsPresence = lhs.presence
let rhsPresence = rhs.presence
if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence {
if lhsPresence.status < rhsPresence.status {
return false
} else if lhsPresence.status > rhsPresence.status {
return true
}
} else if let _ = lhsPresence {
return true
} else if let _ = rhsPresence {
return false
}
return lhs.id < rhs.id
})
}
private final class PeerInfoMembersContextImpl {
private let queue: Queue
private let context: AccountContext
private let peerId: PeerId
private var canAddMembers = false
private var members: [PeerInfoMember] = []
private var dataState: PeerInfoMembersDataState = .loading(isInitial: true)
private var removingMemberIds: [PeerId: Disposable] = [:]
private var membersHidden: Bool?
private var participantsVersion: Int32?
private let stateValue = Promise<PeerInfoMembersState>()
var state: Signal<PeerInfoMembersState, NoError> {
return self.stateValue.get()
}
private let disposable = MetaDisposable()
private let peerDisposable = MetaDisposable()
private var channelMembersControl: PeerChannelMemberCategoryControl?
init(queue: Queue, context: AccountContext, peerId: PeerId) {
self.queue = queue
self.context = context
self.peerId = peerId
self.pushState()
if peerId.namespace == Namespaces.Peer.CloudChannel {
let (disposable, control) = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, requestUpdate: true, updated: { [weak self] state in
queue.async {
guard let strongSelf = self else {
return
}
let unsortedMembers = state.list.map { item -> PeerInfoMember in
return .channelMember(participant: item, storyStats: state.peerStoryStats[item.peer.id])
}
let members: [PeerInfoMember]
if unsortedMembers.count <= 50 {
members = membersSortedByPresence(unsortedMembers, accountPeerId: strongSelf.context.account.peerId)
} else {
members = unsortedMembers
}
strongSelf.members = members
switch state.loadingState {
case let .loading(initial):
strongSelf.dataState = .loading(isInitial: initial)
case let .ready(hasMore):
strongSelf.dataState = .ready(canLoadMore: hasMore)
}
strongSelf.pushState()
}
})
self.disposable.set(disposable)
self.channelMembersControl = control
self.peerDisposable.set((context.account.postbox.peerView(id: peerId)
|> deliverOn(self.queue)).startStrict(next: { [weak self] view in
guard let strongSelf = self else {
return
}
if let channel = peerViewMainPeer(view) as? TelegramChannel {
var canAddMembers = false
switch channel.info {
case .broadcast:
break
case .group:
if channel.flags.contains(.isCreator) || channel.hasPermission(.inviteMembers) {
canAddMembers = true
}
}
strongSelf.canAddMembers = canAddMembers
strongSelf.pushState()
}
var membersHidden: Bool?
if let cachedData = view.cachedData as? CachedChannelData, case let .known(value) = cachedData.membersHidden {
membersHidden = value.value
}
if strongSelf.membersHidden != membersHidden {
let shouldResetList = strongSelf.membersHidden != nil
strongSelf.membersHidden = membersHidden
if shouldResetList, let control = strongSelf.channelMembersControl {
context.peerChannelMemberCategoriesContextsManager.reset(peerId: peerId, control: control)
}
}
}))
} else if peerId.namespace == Namespaces.Peer.CloudGroup {
self.disposable.set((context.account.postbox.peerView(id: peerId)
|> deliverOn(self.queue)).startStrict(next: { [weak self] view in
guard let strongSelf = self, let cachedData = view.cachedData as? CachedGroupData, let participantsData = cachedData.participants else {
return
}
if let currentVersion = strongSelf.participantsVersion, currentVersion > participantsData.version {
return
}
strongSelf.participantsVersion = participantsData.version
var unsortedMembers: [PeerInfoMember] = []
for participant in participantsData.participants {
if let peer = view.peers[participant.peerId] {
let role: PeerInfoMemberRole
let invitedBy: PeerId?
let rank: String?
switch participant {
case let .creator(_, rankValue):
role = .creator
invitedBy = nil
rank = rankValue
case let .admin(_, invitedByValue, _, rankValue):
role = .admin
invitedBy = invitedByValue
rank = rankValue
case let .member(_, invitedByValue, _, rankValue):
role = .member
invitedBy = invitedByValue
rank = rankValue
}
unsortedMembers.append(.legacyGroupMember(peer: RenderedPeer(peer: peer), role: role, invitedBy: invitedBy, presence: view.peerPresences[participant.peerId] as? TelegramUserPresence, storyStats: view.memberStoryStats[participant.peerId], rank: rank))
}
}
if let group = peerViewMainPeer(view) as? TelegramGroup {
var canAddMembers = false
switch group.role {
case .admin, .creator:
canAddMembers = true
case .member:
break
}
if !group.hasBannedPermission(.banAddMembers) {
canAddMembers = true
}
strongSelf.canAddMembers = canAddMembers
}
strongSelf.members = membersSortedByPresence(unsortedMembers, accountPeerId: strongSelf.context.account.peerId)
strongSelf.dataState = .ready(canLoadMore: false)
strongSelf.pushState()
}))
} else {
self.dataState = .ready(canLoadMore: false)
self.pushState()
}
}
deinit {
self.disposable.dispose()
self.peerDisposable.dispose()
for (_, disposable) in self.removingMemberIds {
disposable.dispose()
}
}
private func pushState() {
if self.removingMemberIds.isEmpty {
self.stateValue.set(.single(PeerInfoMembersState(canAddMembers: self.canAddMembers, members: self.members, dataState: self.dataState)))
} else {
self.stateValue.set(.single(PeerInfoMembersState(canAddMembers: self.canAddMembers, members: self.members.filter { member in
return self.removingMemberIds[member.id] == nil
}, dataState: self.dataState)))
}
}
func loadMore() {
if case .ready(true) = self.dataState, let channelMembersControl = self.channelMembersControl {
self.context.peerChannelMemberCategoriesContextsManager.loadMore(peerId: self.peerId, control: channelMembersControl)
}
}
func removeMember(memberId: PeerId) {
if self.removingMemberIds[memberId] == nil {
let signal: Signal<Never, NoError>
if self.peerId.namespace == Namespaces.Peer.CloudChannel {
signal = context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: self.context.engine, peerId: self.peerId, memberId: memberId, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max))
|> ignoreValues
} else {
signal = self.context.engine.peers.removePeerMember(peerId: self.peerId, memberId: memberId)
|> ignoreValues
}
let completed: () -> Void = { [weak self] in
guard let strongSelf = self else {
return
}
if let disposable = strongSelf.removingMemberIds.removeValue(forKey: memberId) {
disposable.dispose()
strongSelf.pushState()
}
}
let disposable = MetaDisposable()
self.removingMemberIds[memberId] = disposable
self.pushState()
disposable.set((signal
|> deliverOn(self.queue)).startStrict(completed: {
completed()
}))
}
}
}
final class PeerInfoMembersContext: Equatable {
private let queue = Queue.mainQueue()
private let impl: QueueLocalObject<PeerInfoMembersContextImpl>
var state: Signal<PeerInfoMembersState, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.state.start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
init(context: AccountContext, peerId: PeerId) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return PeerInfoMembersContextImpl(queue: queue, context: context, peerId: peerId)
})
}
func loadMore() {
self.impl.with { impl in
impl.loadMore()
}
}
func removeMember(memberId: PeerId) {
self.impl.with { impl in
impl.removeMember(memberId: memberId)
}
}
static func ==(lhs: PeerInfoMembersContext, rhs: PeerInfoMembersContext) -> Bool {
return lhs === rhs
}
}