Telegram-iOS/submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift
isaac 493f3103b3 Postbox -> TelegramEngine waves 37-43 + wave 44 design/plan (squashed)
Squashes 20 commits — the implementation and outcome commits of
waves 37 through 43 plus wave 44's spec and implementation-plan
docs — into a single commit. Per-wave lessons remain recorded in
docs/superpowers/postbox-refactor-log.md. The unrelated "Add swift
svg" commit is preserved separately outside this squash.

Wave 37 — peerTokenTitle: peer Peer → EnginePeer (1 file)
Wave 38 — canSendMessagesToPeer: peer Peer → EnginePeer (12 files)
Wave 39 — AccountContext.makePeerInfoController: peer Peer → EnginePeer (52 files)
Wave 40 — makeChatQrCodeScreen + makeChatRecentActionsController bundle (8 files)
Wave 41 — RenderedChannelParticipant.peer: Peer → EnginePeer (28 files)
Wave 42 — PeerInfoScreenData.peer: Peer? → EnginePeer? (17 files)
Wave 43 — PeerInfoScreen 6 helpers: peer Peer? → EnginePeer? (12 files)
Wave 44 — RenderedChannelParticipant.peers design doc + implementation plan
         (impl and outcome land in subsequent commits, not part of squash)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 23:19:43 +04:00

150 lines
7.4 KiB
Swift

import Foundation
import TelegramCore
import SwiftSignalKit
import AccountContext
import StringTransliteration
public enum SearchPeerMembersScope {
case memberSuggestion
case mention
}
public func searchPeerMembers(context: AccountContext, peerId: EnginePeer.Id, chatLocation: ChatLocation, query: String, scope: SearchPeerMembersScope) -> Signal<[EnginePeer], NoError> {
let normalizedQuery = query.lowercased()
let transformedQuery = postboxTransformedString(normalizedQuery as NSString, true, false) ?? normalizedQuery
if peerId.namespace == Namespaces.Peer.CloudChannel {
return context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.ParticipantCount(id: peerId)
)
|> mapToSignal { participantCount -> Signal<([EnginePeer], Bool), NoError> in
if case .peer = chatLocation, let memberCount = participantCount, memberCount <= 64 {
return Signal { subscriber in
let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, requestUpdate: false, updated: { state in
if case .ready = state.loadingState {
subscriber.putNext((state.list.compactMap { participant -> EnginePeer? in
if participant.peer.isDeleted {
return nil
}
if normalizedQuery.isEmpty {
return participant.peer
} else {
if participant.peer.indexName.matchesByTokens(normalizedQuery) || participant.peer.indexName.matchesByTokens(transformedQuery) {
return participant.peer
}
if let addressName = participant.peer.addressName, addressName.lowercased().hasPrefix(normalizedQuery) || addressName.lowercased().hasPrefix(transformedQuery) {
return participant.peer
}
return nil
}
}, true))
}
})
return ActionDisposable {
disposable.dispose()
}
}
|> runOn(Queue.mainQueue())
}
return Signal { subscriber in
switch chatLocation {
case let .peer(peerId):
let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: normalizedQuery.isEmpty ? nil : normalizedQuery, updated: { state in
if case .ready = state.loadingState {
subscriber.putNext((state.list.compactMap { participant in
if participant.peer.isDeleted {
return nil
}
return participant.peer
}, true))
}
})
return ActionDisposable {
disposable.dispose()
}
case let .replyThread(replyThreadMessage):
let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.mentions(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, threadMessageId: EngineMessage.Id(peerId: replyThreadMessage.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: replyThreadMessage.threadId)), searchQuery: normalizedQuery.isEmpty ? nil : normalizedQuery, updated: { state in
if case .ready = state.loadingState {
subscriber.putNext((state.list.compactMap { participant in
if participant.peer.isDeleted {
return nil
}
return participant.peer
}, true))
}
})
return ActionDisposable {
disposable.dispose()
}
case .customChatContents:
subscriber.putNext(([], true))
return ActionDisposable {
}
}
} |> runOn(Queue.mainQueue())
}
|> mapToSignal { result, isReady -> Signal<[EnginePeer], NoError> in
switch scope {
case .mention:
return .single(result)
case .memberSuggestion:
return context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)
|> map { peer -> [EnginePeer] in
var result = result
if isReady {
if case let .channel(channel) = peer, case .group = channel.info {
var matches = false
if normalizedQuery.isEmpty {
matches = true
} else {
if channel.indexName.matchesByTokens(normalizedQuery) || channel.indexName.matchesByTokens(transformedQuery) {
matches = true
}
if let addressName = channel.addressName, addressName.lowercased().hasPrefix(normalizedQuery) || addressName.lowercased().hasPrefix(transformedQuery) {
matches = true
}
}
if matches {
result.insert(.channel(channel), at: 0)
}
}
}
return result
}
}
}
} else {
let transliteratedPeers: Signal<[EnginePeer], NoError>
if transformedQuery != normalizedQuery {
transliteratedPeers = context.engine.peers.searchGroupMembers(peerId: peerId, query: transformedQuery)
} else {
transliteratedPeers = .single([])
}
return combineLatest(
context.engine.peers.searchGroupMembers(peerId: peerId, query: normalizedQuery),
transliteratedPeers
)
|> map { peers, transliteratedPeers -> [EnginePeer] in
var existingPeerIds = Set<EnginePeer.Id>()
var result = peers
for peer in peers {
existingPeerIds.insert(peer.id)
}
for peer in transliteratedPeers {
if !existingPeerIds.contains(peer.id) {
result.append(peer)
}
}
return result
}
}
}