mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-06-20 18:24:43 +00:00
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>
265 lines
13 KiB
Swift
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)
|
|
}
|
|
}
|