Files
isaacandClaude Opus 4.7 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

265 lines
13 KiB
Swift

import Foundation
import UIKit
import Display
import AccountContext
import SwiftSignalKit
import TelegramCore
import ContextUI
import PeerInfoVisualMediaPaneNode
extension PeerInfoScreenNode {
func displayGiftsContextMenu(source: ContextReferenceContentNode, gesture: ContextGesture?) {
guard let currentPaneKey = self.paneContainerNode.currentPaneKey, case .gifts = currentPaneKey else {
return
}
guard let pane = self.paneContainerNode.currentPane?.node as? PeerInfoGiftsPaneNode else {
return
}
guard let controller = self.controller else {
return
}
guard let data = self.data else {
return
}
let giftsContext = pane.giftsContext
var hasVisibility = false
if case let .channel(channel) = data.peer, channel.hasPermission(.sendSomething) {
hasVisibility = true
} else if data.peer?.id == self.context.account.peerId {
hasVisibility = true
}
let isCollection = giftsContext.collectionId != nil
let strings = self.presentationData.strings
let items: Signal<ContextController.Items, NoError> = giftsContext.state
|> map { state in
var hasPinnedGifts = false
for gift in state.gifts {
if gift.pinnedToTop {
hasPinnedGifts = true
break
}
}
return (state.filter, state.sorting, hasPinnedGifts || isCollection)
}
|> distinctUntilChanged(isEqual: { lhs, rhs -> Bool in
let filterEquals = lhs.0 == rhs.0
let sortingEquals = lhs.1 == rhs.1
let canReorderEquals = lhs.2 == rhs.2
return filterEquals && sortingEquals && canReorderEquals
})
|> map { [weak pane, weak giftsContext] filter, sorting, canReorder -> ContextController.Items in
var items: [ContextMenuItem] = []
if hasVisibility {
if let pane, case .all = pane.currentCollection {
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_AddCollection, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/Gifts/AddCollection"), color: theme.contextMenu.primaryColor)
}, action: { [weak pane] _, f in
f(.default)
if let pane {
pane.createCollection()
}
})))
} else {
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_AddGifts, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/Gifts/AddGift"), color: theme.contextMenu.primaryColor)
}, action: { [weak pane] _, f in
f(.default)
if let pane, case let .collection(id) = pane.currentCollection {
pane.addGiftsToCollection(id: id)
}
})))
}
if canReorder {
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Reorder, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor)
}, action: { [weak pane] _, f in
f(.default)
if let pane {
pane.beginReordering()
}
})))
}
if let pane, case let .collection(id) = pane.currentCollection {
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_DeleteCollection, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { [weak pane] _, f in
f(.default)
if let pane {
pane.deleteCollection(id: id)
}
})))
}
}
if let pane, case let .collection(id) = pane.currentCollection, let addressName = data.peer?.addressName, !addressName.isEmpty {
let shareAction: ContextMenuItem = .action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_ShareCollection, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
self?.openShareLink(url: "https://t.me/\(addressName)/c/\(id)")
}))
if items.isEmpty {
items.append(shareAction)
} else {
items.insert(shareAction, at: 1)
}
}
if !items.isEmpty {
items.append(.separator)
}
if let pane, case .all = pane.currentCollection {
items.append(.action(ContextMenuActionItem(text: sorting == .date ? strings.PeerInfo_Gifts_SortByValue : strings.PeerInfo_Gifts_SortByDate, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: sorting == .date ? "Peer Info/SortValue" : "Peer Info/SortDate"), color: theme.contextMenu.primaryColor)
}, action: { [weak giftsContext] _, f in
f(.default)
giftsContext?.updateSorting(sorting == .date ? .value : .date)
})))
items.append(.separator)
}
let toggleFilter: (ProfileGiftsContext.Filters) -> Void = { [weak giftsContext] value in
var updatedFilter = filter
if updatedFilter.contains(value) {
updatedFilter.remove(value)
} else {
updatedFilter.insert(value)
}
if !updatedFilter.contains(.unlimited) && !updatedFilter.contains(.limitedUpgradable) && !updatedFilter.contains(.limitedNonUpgradable) && !updatedFilter.contains(.unique) {
updatedFilter.insert(.unlimited)
}
if !updatedFilter.contains(.displayed) && !updatedFilter.contains(.hidden) {
if value == .displayed {
updatedFilter.insert(.hidden)
} else {
updatedFilter.insert(.displayed)
}
}
giftsContext?.updateFilter(updatedFilter)
}
let switchToFilter: (ProfileGiftsContext.Filters) -> Void = { [weak giftsContext] value in
var updatedFilter = filter
updatedFilter.remove(.unlimited)
updatedFilter.remove(.limitedUpgradable)
updatedFilter.remove(.limitedNonUpgradable)
updatedFilter.remove(.unique)
updatedFilter.insert(value)
giftsContext?.updateFilter(updatedFilter)
}
let switchToVisiblityFilter: (ProfileGiftsContext.Filters) -> Void = { [weak giftsContext] value in
var updatedFilter = filter
updatedFilter.remove(.hidden)
updatedFilter.remove(.displayed)
updatedFilter.insert(value)
giftsContext?.updateFilter(updatedFilter)
}
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Unlimited, icon: { theme in
return filter.contains(.unlimited) ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : UIImage()
}, action: { _, f in
toggleFilter(.unlimited)
}, longPressAction: { _, f in
switchToFilter(.unlimited)
})))
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Limited, icon: { theme in
return filter.contains(.limitedNonUpgradable) ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : UIImage()
}, action: { _, f in
toggleFilter(.limitedNonUpgradable)
}, longPressAction: { _, f in
switchToFilter(.limitedNonUpgradable)
})))
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Upgradable, icon: { theme in
return filter.contains(.limitedUpgradable) ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : UIImage()
}, action: { _, f in
toggleFilter(.limitedUpgradable)
}, longPressAction: { _, f in
switchToFilter(.limitedUpgradable)
})))
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Unique, icon: { theme in
return filter.contains(.unique) ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : UIImage()
}, action: { _, f in
toggleFilter(.unique)
}, longPressAction: { _, f in
switchToFilter(.unique)
})))
if hasVisibility {
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Displayed, icon: { theme in
return filter.contains(.displayed) ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : UIImage()
}, action: { _, f in
toggleFilter(.displayed)
}, longPressAction: { _, f in
switchToVisiblityFilter(.displayed)
})))
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Hidden, icon: { theme in
return filter.contains(.hidden) ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : UIImage()
}, action: { _, f in
toggleFilter(.hidden)
}, longPressAction: { _, f in
switchToVisiblityFilter(.hidden)
})))
}
return ContextController.Items(content: .list(items))
}
var sourceView: UIView = source.view
if sourceView.isDescendant(of: self.headerNode.navigationButtonContainer.rightButtonsBackground) {
sourceView = self.headerNode.navigationButtonContainer.rightButtonsBackground
} else if sourceView.isDescendant(of: self.headerNode.navigationButtonContainer.leftButtonsBackground) {
sourceView = self.headerNode.navigationButtonContainer.leftButtonsBackground
}
let contextController = makeContextController(presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: items, gesture: gesture)
contextController.passthroughTouchEvent = { [weak self] sourceView, point in
guard let strongSelf = self else {
return .ignore
}
let localPoint = strongSelf.view.convert(sourceView.convert(point, to: nil), from: nil)
guard let localResult = strongSelf.hitTest(localPoint, with: nil) else {
return .dismiss(consume: true, result: nil)
}
var testView: UIView? = localResult
while true {
if let testViewValue = testView {
if let node = testViewValue.asyncdisplaykit_node as? PeerInfoHeaderNavigationButton {
node.isUserInteractionEnabled = false
DispatchQueue.main.async {
node.isUserInteractionEnabled = true
}
return .dismiss(consume: false, result: nil)
} else {
testView = testViewValue.superview
}
} else {
break
}
}
return .dismiss(consume: true, result: nil)
}
self.mediaGalleryContextMenu = contextController
controller.presentInGlobalOverlay(contextController)
}
}