Telegram-iOS/submodules/TelegramUI/Sources/ChatControllerUpdateSearch.swift
isaac 86d1456552 Postbox -> TelegramEngine waves 107-137 (squashed)
31 waves of consumer-side migration from `import Postbox` to TelegramEngine
typealiases. Net: 173 import drops + 39 BUILD-dep drops + 1 new typealias
(`EngineStoryId = StoryId`, wave 113).

Wave shapes used:
- Orphan-import sweeps (107, 108, 128): drop `import Postbox` from files
  whose only Postbox-symbol reference was the import line itself, then
  resolve build failures. Methodology requires token-level (`grep -oE`)
  filtering, not line-level, to avoid masking real Postbox usage on lines
  that also contain `Namespaces.X` references.
- Identifier-swap mini-waves (109-127, 129-134, 136-137): rename
  Postbox-typealiased identifiers to engine equivalents
  (PeerId -> EnginePeer.Id, MessageId -> EngineMessage.Id,
  MediaId -> EngineMedia.Id, MessageIndex -> EngineMessage.Index,
  StoryId -> EngineStoryId, ItemCollectionId -> EngineItemCollectionId,
  PreferencesEntry -> EnginePreferencesEntry,
  FetchResourceSourceType/Error -> EngineFetchResourceSourceType/Error,
  MemoryBuffer -> EngineMemoryBuffer, MessageTags -> EngineMessage.Tags,
  MessageAttribute -> EngineMessage.Attribute,
  TempBox -> EngineTempBox).
- Asset-string FP-only orphans (124).
- Typealias addition + drain (113): added `EngineStoryId` typealias to
  TelegramCore, then drained 3+11 consumer sites.

Hard blockers identified during these waves (must restore `import Postbox`
when present): MediaResource[A-Za-z]* (any suffix -- the literal
`MediaResource` matches don't catch MediaResourceData/MediaResourceId/etc.),
Postbox/MediaBox/MediaResource raw types, PostboxCoding/PostboxEncoder/
PostboxDecoder, TempBoxFile, ValueBoxKey, PostboxView, combinedView,
HashFunctions, postboxLog, openPostbox, declareEncodable, PeerView,
MessageHistoryView, MessageHistoryThreadData, CachedPeerData, RenderedPeer,
SelectivePrivacyPeer, SimpleDictionary, ItemCollectionInfosView,
ItemCollectionItem, ItemCollectionItemIndex, ItemCollectionViewEntryIndex,
ChatListIndex, ChatListEntrySummaryComponents, CodableEntry,
MessageHistoryThread, MessageHistoryAnchorIndex,
MessageHistoryEntryLocation, PeerStoryStats, PeerNameIndex,
PeerSummaryCounterTags, ChatListTotalUnreadStateCategory/Stats,
arePeersEqual. Protocol-shape blockers: bare `Peer`/`Message`/`Media`
in function signatures, generic args, enum-case payloads, or dict value
types (e.g., `[PeerId: Peer]`, `case messages([Message])`,
`Signal<(Peer?, ...), NoError>`).

`replace_all PeerId -> EnginePeer.Id` is dangerous: mangles compound
names like `failedPeerId`, `ContactListPeerId`, `nextRemoteMediaId`,
`replyToMessageId`. Pre-flight grep `\b[a-z][a-zA-Z]*PeerId\b` and only
replace_all if 0 matches.

Also removes unneeded design/plan docs from a separate (link-highlighting)
feature branch:
- docs/superpowers/plans/2026-05-02-link-highlighting-modern-path-fixes.md
- docs/superpowers/specs/2026-05-02-link-highlighting-modern-path-fixes-design.md

Squashed commits: 6d82c2980d..e6de5d53a3 (59 commits, including
per-wave content commits and per-wave CLAUDE.md bumps).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:28:50 +02:00

189 lines
10 KiB
Swift

import Foundation
import TelegramPresentationData
import AccountContext
import ChatPresentationInterfaceState
import SwiftSignalKit
import TelegramCore
extension ChatControllerImpl {
func updateSearch(_ interfaceState: ChatPresentationInterfaceState) -> ChatPresentationInterfaceState? {
guard let peerId = self.chatLocation.peerId else {
return nil
}
let limit: Int32 = 100
var derivedSearchState: ChatSearchState?
if let search = interfaceState.search {
func loadMoreStateFromResultsState(_ resultsState: ChatSearchResultsState?) -> SearchMessagesState? {
guard let resultsState = resultsState, let currentId = resultsState.currentId else {
return nil
}
if let index = resultsState.messageIndices.firstIndex(where: { $0.id == currentId }) {
if index <= limit / 2 {
return resultsState.state
}
}
return nil
}
var threadId: Int64?
switch self.chatLocation {
case .peer:
break
case let .replyThread(replyThreadMessage):
threadId = replyThreadMessage.threadId
case .customChatContents:
break
}
var reactions: [MessageReaction.Reaction]?
if !search.query.isEmpty, let historyFilter = interfaceState.historyFilter {
reactions = ReactionsMessageAttribute.reactionFromMessageTag(tag: historyFilter.customTag).flatMap {
[$0]
}
}
switch search.domain {
case .everything:
derivedSearchState = ChatSearchState(query: search.query, location: .peer(peerId: peerId, fromId: nil, tags: nil, reactions: reactions, threadId: threadId, minDate: nil, maxDate: nil), loadMoreState: loadMoreStateFromResultsState(search.resultsState))
case let .tag(reaction):
derivedSearchState = ChatSearchState(query: search.query, location: .peer(peerId: peerId, fromId: nil, tags: nil, reactions: reactions ?? [reaction], threadId: threadId, minDate: nil, maxDate: nil), loadMoreState: loadMoreStateFromResultsState(search.resultsState))
case .members:
derivedSearchState = nil
case let .member(peer):
derivedSearchState = ChatSearchState(query: search.query, location: .peer(peerId: peerId, fromId: peer.id, tags: nil, reactions: reactions, threadId: threadId, minDate: nil, maxDate: nil), loadMoreState: loadMoreStateFromResultsState(search.resultsState))
}
}
if derivedSearchState != self.searchState {
let previousSearchState = self.searchState
self.searchState = derivedSearchState
if let searchState = derivedSearchState {
if previousSearchState?.query != searchState.query || previousSearchState?.location != searchState.location {
var queryIsEmpty = false
if searchState.query.isEmpty {
if case let .peer(_, fromId, _, reactions, _, _, _) = searchState.location {
if fromId == nil {
queryIsEmpty = true
}
if let reactions, !reactions.isEmpty {
queryIsEmpty = false
}
} else {
queryIsEmpty = true
}
}
if queryIsEmpty {
self.searching.set(false)
self.searchResultsCount.set(0)
self.searchDisposable?.set(nil)
self.searchResult.set(.single(nil))
if let data = interfaceState.search {
return interfaceState.updatedSearch(data.withUpdatedResultsState(nil))
}
} else {
self.searching.set(true)
let searchDisposable: MetaDisposable
if let current = self.searchDisposable {
searchDisposable = current
} else {
searchDisposable = MetaDisposable()
self.searchDisposable = searchDisposable
}
let search = self.context.engine.messages.searchMessages(location: searchState.location, query: searchState.query, state: nil, limit: limit)
|> delay(0.2, queue: Queue.mainQueue())
self.searchResult.set(search
|> map { (result, state) -> (SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)? in
return (result, state, searchState.location)
})
searchDisposable.set((search
|> deliverOnMainQueue).startStrict(next: { [weak self] results, updatedState in
guard let strongSelf = self else {
return
}
strongSelf.searchResultsCount.set(results.totalCount)
var navigateIndex: EngineMessage.Index?
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in
if let data = current.search {
let messageIndices = results.messages.map({ $0.index }).sorted()
var currentIndex = messageIndices.last
if let previousResultId = data.resultsState?.currentId {
for index in messageIndices {
if index.id >= previousResultId {
currentIndex = index
break
}
}
}
navigateIndex = currentIndex
return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIndices: messageIndices, currentId: currentIndex?.id, state: updatedState, totalCount: results.totalCount, completed: results.completed)))
} else {
return current
}
})
if let navigateIndex = navigateIndex {
switch strongSelf.chatLocation {
case .peer, .replyThread, .customChatContents:
strongSelf.navigateToMessage(from: nil, to: .index(navigateIndex), forceInCurrentChat: true)
}
}
strongSelf.updateItemNodesSearchTextHighlightStates()
}, completed: { [weak self] in
if let strongSelf = self {
strongSelf.searching.set(false)
}
}))
}
} else if previousSearchState?.loadMoreState != searchState.loadMoreState {
if let loadMoreState = searchState.loadMoreState {
self.searching.set(true)
let searchDisposable: MetaDisposable
if let current = self.searchDisposable {
searchDisposable = current
} else {
searchDisposable = MetaDisposable()
self.searchDisposable = searchDisposable
}
searchDisposable.set((self.context.engine.messages.searchMessages(location: searchState.location, query: searchState.query, state: loadMoreState, limit: limit)
|> delay(0.2, queue: Queue.mainQueue())
|> deliverOnMainQueue).startStrict(next: { [weak self] results, updatedState in
guard let strongSelf = self else {
return
}
strongSelf.searchResultsCount.set(results.totalCount)
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in
if let data = current.search, let previousResultsState = data.resultsState {
let messageIndices = results.messages.map({ $0.index }).sorted()
return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIndices: messageIndices, currentId: previousResultsState.currentId, state: updatedState, totalCount: results.totalCount, completed: results.completed)))
} else {
return current
}
})
}, completed: { [weak self] in
if let strongSelf = self {
strongSelf.searching.set(false)
}
}))
} else {
self.searching.set(false)
self.searchResultsCount.set(0)
self.searchDisposable?.set(nil)
}
}
} else {
self.searching.set(false)
self.searchResultsCount.set(0)
self.searchDisposable?.set(nil)
if let data = interfaceState.search {
return interfaceState.updatedSearch(data.withUpdatedResultsState(nil))
}
}
}
self.updateItemNodesSearchTextHighlightStates()
return nil
}
}