Files
Telegram-iOS/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollOptionMedia.swift
T
isaacisaacClaude Opus 4.7
69bfc65da7 Postbox refactor waves 138–276: 168-commit squash
Consolidates 137 wave commits + 31 supporting commits (CLAUDE.md bump,
typealias additions, AnyObject→EngineMedia restoration) into one squashed
commit. Migrates dozens of consumer-side public APIs, struct fields,
protocol methods, and enum payloads from Postbox protocols/structs to
TelegramEngine engine wrappers and typealiases. Drops `import Postbox`
from many files. Adds new TelegramCore typealiases and one
TelegramEngineUnauthorized facade.

Notable changes by category:

**TelegramCore typealias additions** (rule 2 — narrow utility typealiases):
- EngineChatListIndex, EngineTempBoxFile, EngineItemCollectionItemIndex,
  EngineItemCollectionViewEntryIndex, EngineValueBoxEncryptionParameters,
  EngineMessageAndThreadId, EnginePeerStoryStats, EngineMessageHistoryAnchorIndex,
  EngineChatListTotalUnreadStateCategory, EngineChatListTotalUnreadStateStats,
  EnginePeerSummaryCounterTags, EngineChatListTotalUnreadState,
  EngineItemCacheEntryId, EngineHashFunctions,
  EngineCachedMediaResourceRepresentationResult,
  EngineMediaResourceDataFetchResult, EngineMediaResourceDataFetchError,
  EngineMediaResourceStatus, EngineCachedPeerData

**TelegramCore engine extensions/forwarders**:
- EngineMessage.engineMedia, EngineMessage.enginePeers,
  EngineMessage.adAttribute, EngineMessage.effectivelyIncoming
- engineFileSize forwarder
- TelegramEngine.Resources.clearCachedMediaResources(mediaResourceIds: Set<EngineMediaResource.Id>)
- TelegramEngine.Resources.fetchStatus(id:resourceSize:)
- TelegramEngineUnauthorized.UnauthorizedResources facade with storeResourceData

**Public API/struct migrations to engine types**:
- ChatAvailableMessageActions.banAuthor/banAuthors → EnginePeer?/[EnginePeer]
- WebSessionsContextState.peers → [EnginePeer.Id: EnginePeer]
- CacheUsageStats.peers → [EnginePeer.Id: EnginePeer]
- PeerCommand.peer → EnginePeer
- PeerInfoControllerMode.calls(messages:) → [EngineMessage]
- CallControllerNodeProtocol.updatePeer → EnginePeer params
- ChatHistoryListNode.messageInCurrentHistoryView (and 4 variants) → EngineMessage?
- ChatHistorySearchContainerNode.messageForGallery → EngineMessage?
- PeerInfoPaneNode.findLoadedMessage / ensureMessageIsVisible /
  transitionNodeForGallery → engine-typed
- GalleryHiddenMediaTarget.getTransitionInfo /
  GalleryHiddenMediaManager.findTarget → engine-typed
- ChatPanelInterfaceInteraction.presentReactionDeletionOptions /
  presentBan*MessageOptions → EnginePeer
- DrawingMessageRenderer.messages → [EngineMessage]
- ChatVideoGalleryItemScrubberView.setFetchStatusSignal →
  EngineMediaResource.FetchStatus
- ChannelDiscussionGroupActionSheetItem.peer, VoiceChatPeerEntry.peer,
  VoiceChatFullscreenParticipantItem.peer, MediaStreamComponent.chatPeer,
  MediaStreamVideoComponent.callPeer, ChatMessageContactBubbleContentNode.contactPeer,
  ChatMessageForwardInfoNode.peer, ChatMessageCommentFooterContentNode.replyPeers,
  ChatReportPeerTitlePanelNode.peer, ChatMessageActionUrlAuthController.bot,
  PeerMediaCollectionInterfaceState.peer, ChatMessageCallBubbleContentNode.peopleAvatars,
  ChatLoadingNode.renderedPeer (→ EngineRenderedPeer) — all to engine types

**Wave-71-shadow stored-field migrations** (Postbox Peer/Message → Engine wrapper):
- LegacyCallControllerNode.peer
- CallStatusBarNode.currentPeer

**Dead-code / dead-field removals**:
- CallController.peer, CallControllerNodeV2.account,
  ContactMultiselectionController PeerNameIndex fields,
  preparedChatListNodeViewTransition account: Account param,
  FetchResource.swift entirely (unused function)

**Module-level Postbox import drops**: 30+ files including TelegramRootController,
EditStories, GiftViewScreen, AnimatedStickerUtils, FetchPhotoLibraryImageResource,
PeerInfoGiftsPaneNode, PeerInfoPaneContainerNode, PresentAddMembers,
PeerInfoProfileItems, ChatControllerAdminBanUsers, PresentationData typealiases,
DefaultDayPresentationTheme, ChatListViewTransition, GalleryHiddenMediaManager,
RecentSessionsController, GifContext, AuthorizationSequenceController,
PeerInfoHeaderEditingContentNode, PeerInfoHeaderNode,
PeerAllowedReactionListController, CallControllerNodeV2, and 6 PeerInfo pane files.

**AnyObject restoration**: rule 8 added (never substitute Postbox protocols
with Any/AnyObject) — undid previous AnyObject substitutions in waves 141/143
back to EngineMedia.

Doc maintenance: CLAUDE.md updated to reflect new typealiases and forwarders.

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

202 lines
9.6 KiB
Swift

import Foundation
import UIKit
import SwiftSignalKit
import TelegramCore
import AsyncDisplayKit
import Display
import ContextUI
import UndoUI
import AccountContext
import ChatMessageItemView
import ChatMessageItemCommon
import ChatControllerInteraction
import TelegramStringFormatting
import TelegramPresentationData
import StickerPeekUI
import StickerPackPreviewUI
extension ChatControllerImpl {
func openPollMedia(message: EngineMessage, subject: ChatControllerInteraction.PollMediaSubject) -> Void {
let mediaSubject: GalleryMediaSubject
let media: EngineMedia?
switch subject {
case let .option(option):
media = option.media.flatMap(EngineMedia.init)
mediaSubject = .pollOption(option.opaqueIdentifier)
case let .solution(solution):
media = solution.media.flatMap(EngineMedia.init)
mediaSubject = .pollSolution
}
guard let media else {
return
}
if case let .file(file) = media, file.isSticker || file.isCustomEmoji {
let _ = (self.context.engine.stickers.isStickerSaved(id: file.fileId)
|> deliverOnMainQueue).start(next: { [weak self] isStarred in
guard let self else {
return
}
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: isStarred ? self.presentationData.strings.Stickers_RemoveFromFavorites : self.presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
let _ = (self.context.engine.stickers.toggleStickerSaved(file: file, saved: !isStarred)
|> deliverOnMainQueue).start(next: { [weak self] result in
if let self {
self.present(UndoOverlayController(presentationData: self.presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: !isStarred ? self.presentationData.strings.Conversation_StickerAddedToFavorites : self.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
}
})
})))
var packReference: StickerPackReference?
for attribute in file.attributes {
switch attribute {
case let .Sticker(_, packReferenceValue, _):
packReference = packReferenceValue
break
default:
break
}
}
if let packReference {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StickerPack_ViewPack, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
guard let self, let controllerInteraction = self.controllerInteraction else {
return
}
let controller = StickerPackScreen(context: self.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controllerInteraction.navigationController(), sendSticker: { [weak self] file, sourceNode, sourceRect in
if let self, let controllerInteraction = self.controllerInteraction {
return controllerInteraction.sendSticker(file, false, false, nil, true, sourceNode, sourceRect, nil, [])
} else {
return false
}
})
controllerInteraction.navigationController()?.view.window?.endEditing(true)
controllerInteraction.presentController(controller, nil)
})))
}
let peekController = makePeekController(
presentationData: self.presentationData,
content: StickerPreviewPeekContent(
context: self.context,
theme: self.presentationData.theme,
strings: self.presentationData.strings,
item: .pack(file),
isCreating: false,
menu: items,
openPremiumIntro: {}
),
sourceView: {
return nil
},
activateImmediately: true
)
self.presentInGlobalOverlay(peekController)
})
} else {
let _ = self.context.sharedContext.openChatMessage(OpenChatMessageParams(
context: self.context,
updatedPresentationData: self.controllerInteraction?.updatedPresentationData,
chatLocation: self.chatLocation,
chatFilterTag: nil,
chatLocationContextHolder: nil,
message: message._asMessage(),
mediaSubject: mediaSubject,
standalone: true,
reverseMessageGalleryOrder: false,
navigationController: self.controllerInteraction?.navigationController(),
dismissInput: { [weak self] in
guard let self else {
return
}
self.controllerInteraction?.dismissTextInput()
},
present: { [weak self] controller, arguments, presentationContextType in
guard let self else {
return
}
switch presentationContextType {
case .current:
self.controllerInteraction?.presentControllerInCurrent(controller, arguments)
default:
self.controllerInteraction?.presentController(controller, arguments)
}
},
transitionNode: { [weak self] messageId, media, adjustRect in
var selectedNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
if let self {
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
if let result = itemNode.transitionNode(id: messageId, media: media, adjustRect: adjustRect) {
selectedNode = result
}
}
}
}
return selectedNode
},
addToTransitionSurface: { [weak self] view in
guard let self else {
return
}
self.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: self.chatDisplayNode.historyNode.view)
},
openUrl: { [weak self] url in
guard let self else {
return
}
self.controllerInteraction?.openUrl(.init(url: url, concealed: false, progress: Promise()))
},
openPeer: { [weak self] peer, navigation in
guard let self else {
return
}
self.controllerInteraction?.openPeer(EnginePeer(peer), navigation, nil, .default)
},
callPeer: { [weak self] peerId, isVideo in
guard let self else {
return
}
self.controllerInteraction?.callPeer(peerId, isVideo)
},
openConferenceCall: { [weak self] message in
guard let self else {
return
}
self.controllerInteraction?.openConferenceCall(message)
},
enqueueMessage: { _ in
},
sendSticker: { [weak self] fileReference, sourceNode, sourceRect in
guard let self else {
return false
}
return self.controllerInteraction?.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, []) ?? false
},
sendEmoji: { [weak self] text, attribute in
guard let self else {
return
}
self.controllerInteraction?.sendEmoji(text, attribute, false)
},
setupTemporaryHiddenMedia: { _, _, _ in
},
chatAvatarHiddenMedia: { _, _ in
},
gallerySource: .standaloneMessage(message._asMessage(), mediaSubject)
))
}
}
}