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>
125 lines
4.9 KiB
Swift
125 lines
4.9 KiB
Swift
import Foundation
|
||
import AVFoundation
|
||
import Display
|
||
import SwiftSignalKit
|
||
import TelegramCore
|
||
import AccountContext
|
||
import OverlayStatusController
|
||
import LegacyMediaPickerUI
|
||
import SaveToCameraRoll
|
||
import PresentationDataUtils
|
||
|
||
func saveMediaToFiles(context: AccountContext, fileReference: FileMediaReference, present: @escaping (ViewController, Any?) -> Void) -> Disposable {
|
||
var title: String?
|
||
var performer: String?
|
||
for attribute in fileReference.media.attributes {
|
||
if case let .Audio(_, _, titleValue, performerValue, _) = attribute {
|
||
if let titleValue, !titleValue.isEmpty {
|
||
title = titleValue
|
||
}
|
||
if let performerValue, !performerValue.isEmpty {
|
||
performer = performerValue
|
||
}
|
||
}
|
||
}
|
||
|
||
var signal = fetchMediaData(context: context, userLocation: .other, mediaReference: fileReference.abstract)
|
||
|
||
var cancelImpl: (() -> Void)?
|
||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||
let progressSignal = Signal<Never, NoError> { subscriber in
|
||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||
cancelImpl?()
|
||
}))
|
||
present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||
return ActionDisposable { [weak controller] in
|
||
Queue.mainQueue().async() {
|
||
controller?.dismiss()
|
||
}
|
||
}
|
||
}
|
||
|> runOn(Queue.mainQueue())
|
||
|> delay(0.15, queue: Queue.mainQueue())
|
||
|
||
let progressDisposable = progressSignal.startStrict()
|
||
|
||
let disposable = MetaDisposable()
|
||
signal = signal
|
||
|> afterDisposed {
|
||
Queue.mainQueue().async {
|
||
progressDisposable.dispose()
|
||
}
|
||
}
|
||
cancelImpl = { [weak disposable] in
|
||
disposable?.set(nil)
|
||
}
|
||
disposable.set((signal
|
||
|> deliverOnMainQueue).startStrict(next: { state, _ in
|
||
switch state {
|
||
case .progress:
|
||
break
|
||
case let .data(data):
|
||
if data.isComplete {
|
||
var symlinkPath = data.path + ".mp3"
|
||
try? FileManager.default.removeItem(atPath: symlinkPath)
|
||
let _ = try? FileManager.default.linkItem(atPath: data.path, toPath: symlinkPath)
|
||
|
||
let audioUrl = URL(fileURLWithPath: symlinkPath)
|
||
let audioAsset = AVURLAsset(url: audioUrl)
|
||
|
||
var fileExtension = "mp3"
|
||
if let filename = fileReference.media.fileName {
|
||
if let dotIndex = filename.lastIndex(of: ".") {
|
||
fileExtension = String(filename[filename.index(after: dotIndex)...])
|
||
}
|
||
}
|
||
|
||
var nameComponents: [String] = []
|
||
if let title {
|
||
if let performer {
|
||
nameComponents.append(performer)
|
||
}
|
||
nameComponents.append(title)
|
||
} else {
|
||
var artist: String?
|
||
var title: String?
|
||
for data in audioAsset.commonMetadata {
|
||
if data.commonKey == .commonKeyArtist {
|
||
artist = data.stringValue
|
||
}
|
||
if data.commonKey == .commonKeyTitle {
|
||
title = data.stringValue
|
||
}
|
||
}
|
||
if let artist, !artist.isEmpty {
|
||
nameComponents.append(artist)
|
||
}
|
||
if let title, !title.isEmpty {
|
||
nameComponents.append(title)
|
||
}
|
||
if nameComponents.isEmpty, var filename = fileReference.media.fileName {
|
||
if let dotIndex = filename.lastIndex(of: ".") {
|
||
filename = String(filename[..<dotIndex])
|
||
}
|
||
nameComponents.append(filename)
|
||
}
|
||
}
|
||
if !nameComponents.isEmpty {
|
||
try? FileManager.default.removeItem(atPath: symlinkPath)
|
||
|
||
let fileName = "\(nameComponents.joined(separator: " – ")).\(fileExtension)"
|
||
symlinkPath = symlinkPath.replacingOccurrences(of: audioUrl.lastPathComponent, with: fileName)
|
||
let _ = try? FileManager.default.linkItem(atPath: data.path, toPath: symlinkPath)
|
||
}
|
||
|
||
let url = URL(fileURLWithPath: symlinkPath)
|
||
let controller = legacyICloudFilePicker(theme: presentationData.theme, mode: .export, url: url, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { _ in
|
||
|
||
})
|
||
present(controller, nil)
|
||
}
|
||
}
|
||
}))
|
||
|
||
return disposable
|
||
}
|