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

280 lines
15 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import MediaResources
import LegacyComponents
import AccountContext
import LegacyUI
import ImageCompression
import LocalMediaResources
import AppBundle
import LegacyMediaPickerUI
import ChatPresentationInterfaceState
import ChatSendButtonRadialStatusNode
public final class InstantVideoController: LegacyController, StandalonePresentableController {
private var captureController: TGVideoMessageCaptureController?
public var onDismiss: ((Bool) -> Void)?
public var onStop: (() -> Void)?
public var didStop: (() -> Void)?
private let micLevelValue = ValuePromise<Float>(0.0)
private let durationValue = ValuePromise<TimeInterval>(0.0)
public let audioStatus: InstantVideoControllerRecordingStatus
private var completed = false
private var dismissed = false
override public init(presentation: LegacyControllerPresentation, theme: PresentationTheme?, strings: PresentationStrings? = nil, initialLayout: ContainerViewLayout? = nil) {
self.audioStatus = InstantVideoControllerRecordingStatus(micLevel: self.micLevelValue.get(), duration: self.durationValue.get())
super.init(presentation: presentation, theme: theme, initialLayout: initialLayout)
self.hasSparseContainerView = true
self.lockOrientation = true
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func bindCaptureController(_ captureController: TGVideoMessageCaptureController?) {
self.captureController = captureController
if let captureController = captureController {
captureController.view.disablesInteractiveKeyboardGestureRecognizer = true
captureController.view.disablesInteractiveTransitionGestureRecognizer = true
captureController.micLevel = { [weak self] (level: CGFloat) -> Void in
self?.micLevelValue.set(Float(level))
}
captureController.onDuration = { [weak self] duration in
self?.durationValue.set(duration)
}
captureController.onDismiss = { [weak self] _, isCancelled in
guard let self = self else { return }
if !self.dismissed {
self.dismissed = true
self.onDismiss?(isCancelled)
}
}
captureController.didStop = { [weak self] in
guard let self else { return }
self.didStop?()
}
captureController.onStop = { [weak self] in
self?.onStop?()
}
}
}
public func dismissVideo() {
if let captureController = self.captureController, !self.dismissed {
self.dismissed = true
captureController.dismiss(true)
}
}
public func extractVideoSnapshot() -> UIView? {
self.captureController?.extractVideoContent()
}
public func hideVideoSnapshot() {
self.captureController?.hideVideoContent()
}
public func completeVideo() {
if let captureController = self.captureController, !self.completed {
self.completed = true
captureController.complete()
}
}
public func dismissAnimated() {
if let captureController = self.captureController, !self.dismissed {
self.dismissed = true
captureController.dismiss(false)
}
}
public func stopVideo() -> Bool {
if let captureController = self.captureController {
return captureController.stop()
}
return false
}
public func lockVideo() {
if let captureController = self.captureController {
return captureController.setLocked()
}
}
public func updateRecordButtonInteraction(_ value: CGFloat) {
if let captureController = self.captureController {
captureController.buttonInteractionUpdate(CGPoint(x: value, y: 0.0))
}
}
public func send() {
if let captureController = self.captureController {
captureController.send()
}
}
}
public func legacyInputMicPalette(from theme: PresentationTheme) -> TGModernConversationInputMicPallete {
let inputPanelTheme = theme.chat.inputPanel
return TGModernConversationInputMicPallete(dark: theme.overallDarkAppearance, buttonColor: inputPanelTheme.actionControlFillColor, iconColor: inputPanelTheme.actionControlForegroundColor, backgroundColor: theme.rootController.navigationBar.opaqueBackgroundColor, borderColor: inputPanelTheme.panelSeparatorColor, lock: inputPanelTheme.panelControlAccentColor, textColor: inputPanelTheme.primaryTextColor, secondaryTextColor: inputPanelTheme.secondaryTextColor, recording: inputPanelTheme.mediaRecordingDotColor)
}
public func legacyInstantVideoController(theme: PresentationTheme, forStory: Bool, panelFrame: CGRect, context: AccountContext, peerId: EnginePeer.Id, slowmodeState: ChatSlowmodeState?, hasSchedule: Bool, send: @escaping (InstantVideoController, EnqueueMessage?) -> Void, displaySlowmodeTooltip: @escaping (UIView, CGRect) -> Void, presentSchedulePicker: @escaping (@escaping (Int32, Bool) -> Void) -> Void) -> InstantVideoController {
let isSecretChat = peerId.namespace == Namespaces.Peer.SecretChat
let legacyController = InstantVideoController(presentation: .custom, theme: theme)
legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .all)
legacyController.lockOrientation = true
legacyController.statusBar.statusBarStyle = .Hide
let baseController = TGViewController(context: legacyController.context)!
baseController.view.isUserInteractionEnabled = false
legacyController.bind(controller: baseController)
legacyController.presentationCompleted = { [weak legacyController, weak baseController] in
if let legacyController = legacyController, let baseController = baseController {
legacyController.view.disablesInteractiveTransitionGestureRecognizer = true
var uploadInterface: LegacyLiveUploadInterface?
if peerId.namespace != Namespaces.Peer.SecretChat {
uploadInterface = LegacyLiveUploadInterface(context: context)
}
var slowmodeValidUntil: Int32 = 0
if let slowmodeState = slowmodeState, case let .timestamp(timestamp) = slowmodeState.variant {
slowmodeValidUntil = timestamp
}
let controller = TGVideoMessageCaptureController(context: legacyController.context, forStory: forStory, assets: TGVideoMessageCaptureControllerAssets(send: PresentationResourcesChat.chatInputPanelSendButtonImage(theme)!, slideToCancel: PresentationResourcesChat.chatInputPanelMediaRecordingCancelArrowImage(theme)!, actionDelete: generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.chat.inputPanel.panelControlAccentColor))!, transitionInView: {
return nil
}, parentController: baseController, controlsFrame: panelFrame, isAlreadyLocked: {
return false
}, liveUploadInterface: uploadInterface, pallete: legacyInputMicPalette(from: theme), slowmodeTimestamp: slowmodeValidUntil, slowmodeView: {
let node = ChatSendButtonRadialStatusView(color: theme.chat.inputPanel.panelControlAccentColor)
node.slowmodeState = slowmodeState
return node
}, canSendSilently: !isSecretChat, canSchedule: hasSchedule, reminder: peerId == context.account.peerId)!
controller.presentScheduleController = { done in
presentSchedulePicker { time, silentPosting in
done?(time, silentPosting)
}
}
controller.finishedWithVideo = { [weak legacyController] videoUrl, previewImage, _, duration, dimensions, liveUploadData, adjustments, isSilent, scheduleTimestamp in
guard let legacyController = legacyController else {
return
}
guard let videoUrl = videoUrl else {
send(legacyController, nil)
return
}
var finalDimensions: CGSize = dimensions
var finalDuration: Double = duration
var previewRepresentations: [TelegramMediaImageRepresentation] = []
if let previewImage = previewImage {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
let thumbnailSize = finalDimensions.aspectFitted(CGSize(width: 320.0, height: 320.0))
let thumbnailImage = TGScaleImageToPixelSize(previewImage, thumbnailSize)!
if let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) {
context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: thumbnailData)
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
}
}
finalDimensions = TGMediaVideoConverter.dimensions(for: finalDimensions, adjustments: adjustments, preset: TGMediaVideoConversionPresetVideoMessage)
var resourceAdjustments: VideoMediaResourceAdjustments?
if let adjustments = adjustments {
if adjustments.trimApplied() {
finalDuration = adjustments.trimEndValue - adjustments.trimStartValue
}
if let dict = adjustments.dictionary(), let data = try? NSKeyedArchiver.archivedData(withRootObject: dict, requiringSecureCoding: false) {
let adjustmentsData = EngineMemoryBuffer(data: data)
let digest = EngineMemoryBuffer(data: adjustmentsData.md5Digest())
resourceAdjustments = VideoMediaResourceAdjustments(data: adjustmentsData, digest: digest, isStory: false)
}
}
if finalDuration.isZero || finalDuration.isNaN {
return
}
let resource: TelegramMediaResource
if let liveUploadData = liveUploadData as? LegacyLiveUploadInterfaceResult, resourceAdjustments == nil, let data = try? Data(contentsOf: videoUrl) {
resource = LocalFileMediaResource(fileId: liveUploadData.id)
context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true)
} else {
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: Int64.min ... Int64.max), path: videoUrl.path, adjustments: resourceAdjustments)
}
if let previewImage = previewImage {
let tempFile = EngineTempBox.shared.tempFile(fileName: "file")
defer {
EngineTempBox.shared.dispose(tempFile)
}
if let data = compressImageToJPEG(previewImage, quality: 0.7, tempFilePath: tempFile.path) {
context.account.postbox.mediaBox.storeCachedResourceRepresentation(resource, representation: CachedVideoFirstFrameRepresentation(), data: data)
}
}
let media = TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: finalDuration, size: PixelDimensions(finalDimensions), flags: [.instantRoundVideo], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])
var message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])
let scheduleTime: Int32? = scheduleTimestamp > 0 ? scheduleTimestamp : nil
message = message.withUpdatedAttributes { attributes in
var attributes = attributes
for i in (0 ..< attributes.count).reversed() {
if attributes[i] is NotificationInfoMessageAttribute {
attributes.remove(at: i)
} else if let _ = scheduleTime, attributes[i] is OutgoingScheduleInfoMessageAttribute {
attributes.remove(at: i)
}
}
if isSilent {
attributes.append(NotificationInfoMessageAttribute(flags: .muted))
}
if let scheduleTime = scheduleTime {
attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime, repeatPeriod: nil))
}
return attributes
}
send(legacyController, message)
}
controller.didDismiss = { [weak legacyController] in
if let legacyController = legacyController {
legacyController.dismiss()
}
}
controller.displaySlowmodeTooltip = { [weak legacyController, weak controller] in
if let legacyController = legacyController, let controller = controller {
let rect = controller.frameForSendButton()
displaySlowmodeTooltip(legacyController.displayNode.view, rect)
}
}
legacyController.bindCaptureController(controller)
let presentationDisposable = context.sharedContext.presentationData.start(next: { [weak controller] presentationData in
if let controller = controller {
controller.pallete = legacyInputMicPalette(from: presentationData.theme)
}
})
legacyController.disposables.add(presentationDisposable)
}
}
return legacyController
}