Files
Telegram-iOS/submodules/TelegramUI/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift
T
isaacisaacClaude 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

242 lines
11 KiB
Swift

import Foundation
import UIKit
import Display
import AccountContext
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import LegacyComponents
import LegacyUI
import AttachmentUI
import MediaPickerUI
import LegacyCamera
import LegacyMediaPickerUI
import LocationUI
import AttachmentFileController
import ChatEntityKeyboardInputNode
import ICloudResources
public enum PollAttachmentSubject {
case description
case quizAnswer
case option
}
public func presentPollAttachmentScreen(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
subject: PollAttachmentSubject,
availableButtons: [AttachmentButtonType],
inputMediaNodeData: Signal<ChatEntityKeyboardInputNode.InputData?, NoError>? = nil,
present: @escaping (ViewController, Bool) -> Void,
completion: @escaping (AnyMediaReference) -> Void
) {
let attachmentController = AttachmentController(
context: context,
updatedPresentationData: updatedPresentationData,
style: .glass,
chatLocation: nil,
isScheduledMessages: false,
buttons: availableButtons,
initialButton: .gallery
)
let inputMediaNodeDataPromise = Promise<ChatEntityKeyboardInputNode.InputData?>(nil)
if let inputMediaNodeData {
inputMediaNodeDataPromise.set(inputMediaNodeData)
} else if availableButtons.contains(.sticker) || availableButtons.contains(.emoji) {
inputMediaNodeDataPromise.set(.single(nil) |> then(
ChatEntityKeyboardInputNode.inputData(
context: context,
chatPeerId: nil,
areCustomEmojiEnabled: true,
hasTrending: false,
hasSearch: true,
hasStickers: true,
hasGifs: false,
hideBackground: true,
maskEdge: .fade,
sendGif: nil
)
|> map(Optional.init)
))
}
attachmentController.requestController = { [weak attachmentController] type, controllerCompletion in
let mediaPickerPollSubject: MediaPickerScreenImpl.Subject.AssetsMode.PollMode
let filePickerPollSubject: AttachmentFileControllerSource.PollMode
let locationPickerPollSubject: LocationPickerController.Source.PollMode
let stickerPickerPollSubject: StickerAttachmentScreen.Source.PollMode
switch subject {
case .description:
mediaPickerPollSubject = .description
filePickerPollSubject = .description
locationPickerPollSubject = .description
stickerPickerPollSubject = .description
case .quizAnswer:
mediaPickerPollSubject = .quizAnswer
filePickerPollSubject = .quizAnswer
locationPickerPollSubject = .quizAnswer
stickerPickerPollSubject = .quizAnswer
default:
mediaPickerPollSubject = .option
filePickerPollSubject = .description
locationPickerPollSubject = .option
stickerPickerPollSubject = .option
}
switch type {
case .gallery:
let controller = MediaPickerScreenImpl(
context: context,
updatedPresentationData: updatedPresentationData,
style: .glass,
peer: nil,
threadTitle: nil,
chatLocation: nil,
enableMultiselection: false,
subject: .assets(nil, .poll(mode: mediaPickerPollSubject, asFile: false))
)
controller.getCaptionPanelView = {
return nil
}
controller.legacyCompletion = { _, signals, _, _, _, _, sendCompletion in
let _ = (legacyAssetPickerEnqueueMessages(context: context, account: context.account, signals: signals)
|> deliverOnMainQueue).start(next: { items in
if let item = items.first, case let .message(_, _, _, mediaReference, _, _, _, _, _, _) = item.message, let mediaReference {
completion(mediaReference)
sendCompletion()
}
})
}
controllerCompletion(controller, controller.mediaPickerContext)
return true
case .file:
let controller = makeAttachmentFileControllerImpl(
context: context,
updatedPresentationData: updatedPresentationData,
source: .poll(filePickerPollSubject),
bannedSendMedia: nil,
presentGallery: { [weak attachmentController] in
attachmentController?.dismiss(animated: true)
let controller = MediaPickerScreenImpl(
context: context,
updatedPresentationData: updatedPresentationData,
style: .glass,
peer: nil,
threadTitle: nil,
chatLocation: nil,
enableMultiselection: false,
subject: .assets(nil, .poll(mode: mediaPickerPollSubject, asFile: true))
)
controller.getCaptionPanelView = {
return nil
}
controller.legacyCompletion = { _, signals, _, _, _, _, sendCompletion in
let _ = (legacyAssetPickerEnqueueMessages(context: context, account: context.account, signals: signals)
|> deliverOnMainQueue).start(next: { items in
if let item = items.first, case let .message(_, _, _, mediaReference, _, _, _, _, _, _) = item.message, let mediaReference {
completion(mediaReference)
sendCompletion()
}
})
}
controller.navigationPresentation = .modal
present(controller, true)
},
presentFiles: { [weak attachmentController] in
attachmentController?.dismiss(animated: true)
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
let controller = legacyICloudFilePicker(theme: presentationData.theme, documentTypes: ["public.item"], completion: { urls in
guard let url = urls.first else {
return
}
let _ = (iCloudFileDescription(url)
|> deliverOnMainQueue).start(next: { item in
guard let item else {
return
}
let fileId = Int64.random(in: Int64.min ... Int64.max)
let mimeType = guessMimeTypeByFileExtension((item.fileName as NSString).pathExtension)
var previewRepresentations: [TelegramMediaImageRepresentation] = []
if mimeType.hasPrefix("image/") || mimeType == "application/pdf" {
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 320, height: 320), resource: ICloudFileResource(urlData: item.urlData, thumbnail: true), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
}
var attributes: [TelegramMediaFileAttribute] = []
attributes.append(.FileName(fileName: item.fileName))
if let audioMetadata = item.audioMetadata {
attributes.append(.Audio(isVoice: false, duration: audioMetadata.duration, title: audioMetadata.title, performer: audioMetadata.performer, waveform: nil))
}
let file = TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes, alternativeRepresentations: [])
completion(.standalone(media: file))
})
})
present(controller, false)
},
presentDocumentScanner: nil,
send: { mediaReferences, _, _, _ in
completion(mediaReferences.first!)
}
) as! AttachmentFileControllerImpl
controllerCompletion(controller, controller.mediaPickerContext)
return true
case .location:
let controller = LocationPickerController(
context: context,
style: .glass,
updatedPresentationData: updatedPresentationData,
mode: .share(peer: nil, selfPeer: nil, hasLiveLocation: false),
source: .poll(locationPickerPollSubject),
completion: { location, _, _, _, _ in
completion(.standalone(media: location))
})
controllerCompletion(controller, controller.mediaPickerContext)
return true
case .sticker:
let _ = (inputMediaNodeDataPromise.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue).start(next: { content in
guard let content = content?.stickers else {
return
}
let controller = StickerAttachmentScreen(
context: context,
mode: .stickers(content),
source: .poll(stickerPickerPollSubject),
completion: { sticker in
completion(sticker)
}
)
controllerCompletion(controller, controller.mediaPickerContext)
})
return true
case .emoji:
let _ = (inputMediaNodeDataPromise.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue).start(next: { content in
guard let content = content?.emoji else {
return
}
let controller = StickerAttachmentScreen(
context: context,
mode: .emoji(content),
source: .poll(stickerPickerPollSubject),
completion: { sticker in
completion(sticker)
}
)
controllerCompletion(controller, controller.mediaPickerContext)
})
return true
default:
return false
}
}
attachmentController.navigationPresentation = .flatModal
present(attachmentController, true)
}