Telegram-iOS/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.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

239 lines
12 KiB
Swift

import Foundation
import TelegramPresentationData
import AccountContext
import TelegramCore
import SwiftSignalKit
import CalendarMessageScreen
import ContextUI
import ChatControllerInteraction
import Display
import UIKit
import UndoUI
import ChatScheduleTimeController
extension ChatControllerImpl {
func openCalendarSearch(timestamp: Int32, isMedia: Bool) {
guard let peerId = self.chatLocation.peerId else {
return
}
self.chatDisplayNode.dismissInput()
if isMedia {
let initialTimestamp = timestamp
var dismissCalendarScreen: (() -> Void)?
var selectDay: ((Int32) -> Void)?
var openClearHistory: ((Int32) -> Void)?
let enableMessageRangeDeletion: Bool = peerId.namespace == Namespaces.Peer.CloudUser
let displayMedia = self.presentationInterfaceState.historyFilter == nil
let calendarScreen = CalendarMessageScreen(
context: self.context,
peerId: peerId,
calendarSource: self.context.engine.messages.sparseMessageCalendar(peerId: peerId, threadId: self.chatLocation.threadId, tag: .photoOrVideo, displayMedia: displayMedia),
initialTimestamp: initialTimestamp,
enableMessageRangeDeletion: enableMessageRangeDeletion,
canNavigateToEmptyDays: true,
navigateToDay: { [weak self] c, index, timestamp in
guard let strongSelf = self else {
c.dismiss()
return
}
strongSelf.alwaysShowSearchResultsAsList = false
strongSelf.chatDisplayNode.alwaysShowSearchResultsAsList = false
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in
return state.updatedDisplayHistoryFilterAsList(false).updatedSearch(nil)
})
c.dismiss()
strongSelf.loadingMessage.set(.single(.generic))
let peerId: EnginePeer.Id
let threadId: Int64?
switch strongSelf.chatLocation {
case let .peer(peerIdValue):
peerId = peerIdValue
threadId = nil
case let .replyThread(replyThreadMessage):
peerId = replyThreadMessage.peerId
threadId = replyThreadMessage.threadId
case .customChatContents:
return
}
strongSelf.messageIndexDisposable.set((strongSelf.context.engine.messages.searchMessageIdByTimestamp(peerId: peerId, threadId: threadId, timestamp: timestamp) |> deliverOnMainQueue).startStrict(next: { messageId in
if let strongSelf = self {
strongSelf.loadingMessage.set(.single(nil))
if let messageId = messageId {
strongSelf.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: true)
}
}
}))
},
previewDay: { [weak self] timestamp, _, sourceNode, sourceRect, gesture in
guard let strongSelf = self else {
return
}
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Chat_JumpToDate, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
dismissCalendarScreen?()
strongSelf.loadingMessage.set(.single(.generic))
let peerId: EnginePeer.Id
let threadId: Int64?
switch strongSelf.chatLocation {
case let .peer(peerIdValue):
peerId = peerIdValue
threadId = nil
case let .replyThread(replyThreadMessage):
peerId = replyThreadMessage.peerId
threadId = replyThreadMessage.threadId
case .customChatContents:
return
}
strongSelf.messageIndexDisposable.set((strongSelf.context.engine.messages.searchMessageIdByTimestamp(peerId: peerId, threadId: threadId, timestamp: timestamp) |> deliverOnMainQueue).startStrict(next: { messageId in
if let strongSelf = self {
strongSelf.loadingMessage.set(.single(nil))
if let messageId = messageId {
strongSelf.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: true)
}
}
}))
})))
if enableMessageRangeDeletion && (peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat) {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.DialogList_ClearHistoryConfirmation, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { _, f in
f(.dismissWithoutContent)
openClearHistory?(timestamp)
})))
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Select, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
selectDay?(timestamp)
})))
}
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: .message(id: .timestamp(timestamp), highlight: nil, timecode: nil, setupReply: false), botStart: nil, mode: .standard(.previewing), params: nil)
chatController.canReadHistory.set(false)
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
strongSelf.presentInGlobalOverlay(contextController)
}
)
calendarScreen.completedWithRemoveMessagesInRange = { [weak self] range, type, dayCount, calendarSource in
guard let strongSelf = self else {
return
}
let statusText: String
switch type {
case .forEveryone:
statusText = strongSelf.presentationData.strings.Chat_MessageRangeDeleted_ForBothSides(Int32(dayCount))
default:
statusText = strongSelf.presentationData.strings.Chat_MessageRangeDeleted_ForMe(Int32(dayCount))
}
strongSelf.chatDisplayNode.historyNode.ignoreMessagesInTimestampRange = range
strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: statusText), text: nil), elevatedLayout: false, action: { value in
guard let strongSelf = self else {
return false
}
if value == .commit {
let _ = calendarSource.removeMessagesInRange(minTimestamp: range.lowerBound, maxTimestamp: range.upperBound, type: type, completion: {
Queue.mainQueue().after(1.0, {
guard let strongSelf = self else {
return
}
strongSelf.chatDisplayNode.historyNode.ignoreMessagesInTimestampRange = nil
})
})
return true
} else if value == .undo {
strongSelf.chatDisplayNode.historyNode.ignoreMessagesInTimestampRange = nil
return true
}
return false
}), in: .current)
}
self.effectiveNavigationController?.pushViewController(calendarScreen)
dismissCalendarScreen = { [weak calendarScreen] in
calendarScreen?.dismiss(completion: nil)
}
selectDay = { [weak calendarScreen] timestamp in
calendarScreen?.selectDay(timestamp: timestamp)
}
openClearHistory = { [weak calendarScreen] timestamp in
calendarScreen?.openClearHistory(timestamp: timestamp)
}
} else {
let controller = ChatScheduleTimeScreen(
context: self.context,
mode: .search,
currentTime: nil,
currentRepeatPeriod: nil,
minimalTime: nil,
isDark: false,
completion: { [weak self] result in
guard let self else {
return
}
self.alwaysShowSearchResultsAsList = false
self.chatDisplayNode.alwaysShowSearchResultsAsList = false
self.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in
return state.updatedDisplayHistoryFilterAsList(false).updatedSearch(nil)
})
self.loadingMessage.set(.single(.generic))
let peerId: EnginePeer.Id
let threadId: Int64?
switch self.chatLocation {
case let .peer(peerIdValue):
peerId = peerIdValue
threadId = nil
case let .replyThread(replyThreadMessage):
peerId = replyThreadMessage.peerId
threadId = replyThreadMessage.threadId
case .customChatContents:
return
}
self.messageIndexDisposable.set((self.context.engine.messages.searchMessageIdByTimestamp(peerId: peerId, threadId: threadId, timestamp: result.time) |> deliverOnMainQueue).startStrict(next: { [weak self] messageId in
guard let self else {
return
}
self.loadingMessage.set(.single(nil))
if let messageId {
self.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: true)
}
}))
}
)
self.push(controller)
}
}
}