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

296 lines
12 KiB
Swift

import Foundation
import UIKit
import SwiftSignalKit
import CoreLocation
import TelegramCore
import TelegramUIPreferences
import PersistentStringHash
import AccountContext
public struct MediaEditorResultPrivacy: Codable, Equatable {
private enum CodingKeys: String, CodingKey {
case sendAsPeerId
case privacy
case timeout
case disableForwarding
case archive
case folderIds
}
public let sendAsPeerId: EnginePeer.Id?
public let privacy: EngineStoryPrivacy
public let timeout: Int
public let isForwardingDisabled: Bool
public let pin: Bool
public let folderIds: [Int64]
public init(
sendAsPeerId: EnginePeer.Id?,
privacy: EngineStoryPrivacy,
timeout: Int,
isForwardingDisabled: Bool,
pin: Bool,
folderIds: [Int64]
) {
self.sendAsPeerId = sendAsPeerId
self.privacy = privacy
self.timeout = timeout
self.isForwardingDisabled = isForwardingDisabled
self.pin = pin
self.folderIds = folderIds
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.sendAsPeerId = try container.decodeIfPresent(Int64.self, forKey: .sendAsPeerId).flatMap { EnginePeer.Id($0) }
self.privacy = try container.decode(EngineStoryPrivacy.self, forKey: .privacy)
self.timeout = Int(try container.decode(Int32.self, forKey: .timeout))
self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .disableForwarding) ?? false
self.pin = try container.decode(Bool.self, forKey: .archive)
self.folderIds = try container.decodeIfPresent([Int64].self, forKey: .folderIds) ?? []
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(self.sendAsPeerId?.toInt64(), forKey: .sendAsPeerId)
try container.encode(self.privacy, forKey: .privacy)
try container.encode(Int32(self.timeout), forKey: .timeout)
try container.encode(self.isForwardingDisabled, forKey: .disableForwarding)
try container.encode(self.pin, forKey: .archive)
try container.encode(self.folderIds, forKey: .folderIds)
}
}
public final class MediaEditorDraft: Codable, Equatable {
enum ReadError: Error {
case generic
}
public static func == (lhs: MediaEditorDraft, rhs: MediaEditorDraft) -> Bool {
return lhs.path == rhs.path
}
private enum CodingKeys: String, CodingKey {
case path
case isVideo
case thumbnail
case dimensionsWidth
case dimensionsHeight
case duration
case values
case caption
case privacy
case forwardInfo
case timestamp
case locationLatitude
case locationLongitude
case expiresOn
}
public let path: String
public let isVideo: Bool
public let thumbnail: UIImage
public let dimensions: PixelDimensions
public let duration: Double?
public let values: MediaEditorValues
public let caption: NSAttributedString
public let privacy: MediaEditorResultPrivacy?
public let forwardInfo: EngineStoryId?
public let timestamp: Int32
public let location: CLLocationCoordinate2D?
public let expiresOn: Int32?
public init(path: String, isVideo: Bool, thumbnail: UIImage, dimensions: PixelDimensions, duration: Double?, values: MediaEditorValues, caption: NSAttributedString, privacy: MediaEditorResultPrivacy?, forwardInfo: EngineStoryId?, timestamp: Int32, location: CLLocationCoordinate2D?, expiresOn: Int32?) {
self.path = path
self.isVideo = isVideo
self.thumbnail = thumbnail
self.dimensions = dimensions
self.duration = duration
self.values = values
self.caption = caption
self.privacy = privacy
self.forwardInfo = forwardInfo
self.timestamp = timestamp
self.location = location
self.expiresOn = expiresOn
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.path = try container.decode(String.self, forKey: .path)
self.isVideo = try container.decode(Bool.self, forKey: .isVideo)
let thumbnailData = try container.decode(Data.self, forKey: .thumbnail)
if let thumbnail = UIImage(data: thumbnailData) {
self.thumbnail = thumbnail
} else {
throw ReadError.generic
}
self.dimensions = PixelDimensions(
width: try container.decode(Int32.self, forKey: .dimensionsWidth),
height: try container.decode(Int32.self, forKey: .dimensionsHeight)
)
self.duration = try container.decodeIfPresent(Double.self, forKey: .duration)
let valuesData = try container.decode(Data.self, forKey: .values)
if let values = try? JSONDecoder().decode(MediaEditorValues.self, from: valuesData) {
self.values = values
} else {
throw ReadError.generic
}
self.caption = ((try? container.decode(ChatTextInputStateText.self, forKey: .caption)) ?? ChatTextInputStateText()).attributedText()
if let data = try container.decodeIfPresent(Data.self, forKey: .privacy), let privacy = try? JSONDecoder().decode(MediaEditorResultPrivacy.self, from: data) {
self.privacy = privacy
} else {
self.privacy = nil
}
self.forwardInfo = try container.decodeIfPresent(EngineStoryId.self, forKey: .forwardInfo)
self.timestamp = try container.decodeIfPresent(Int32.self, forKey: .timestamp) ?? 1688909663
if let latitude = try container.decodeIfPresent(Double.self, forKey: .locationLatitude), let longitude = try container.decodeIfPresent(Double.self, forKey: .locationLongitude) {
self.location = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
} else {
self.location = nil
}
self.expiresOn = try container.decodeIfPresent(Int32.self, forKey: .expiresOn)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.path, forKey: .path)
try container.encode(self.isVideo, forKey: .isVideo)
if let thumbnailData = self.thumbnail.jpegData(compressionQuality: 0.6) {
try container.encode(thumbnailData, forKey: .thumbnail)
}
try container.encode(self.dimensions.width, forKey: .dimensionsWidth)
try container.encode(self.dimensions.height, forKey: .dimensionsHeight)
try container.encodeIfPresent(self.duration, forKey: .duration)
if let valuesData = try? JSONEncoder().encode(self.values) {
try container.encode(valuesData, forKey: .values)
}
let chatInputText = ChatTextInputStateText(attributedText: self.caption)
try container.encode(chatInputText, forKey: .caption)
if let privacy = self.privacy {
if let data = try? JSONEncoder().encode(privacy) {
try container.encode(data, forKey: .privacy)
} else {
try container.encodeNil(forKey: .privacy)
}
} else {
try container.encodeNil(forKey: .privacy)
}
try container.encodeIfPresent(self.forwardInfo, forKey: .forwardInfo)
try container.encode(self.timestamp, forKey: .timestamp)
if let location = self.location {
try container.encode(location.latitude, forKey: .locationLatitude)
try container.encode(location.longitude, forKey: .locationLongitude)
} else {
try container.encodeNil(forKey: .locationLatitude)
try container.encodeNil(forKey: .locationLongitude)
}
try container.encodeIfPresent(self.expiresOn, forKey: .expiresOn)
}
}
private struct MediaEditorDraftItemId {
public let rawValue: EngineMemoryBuffer
var value: Int64 {
return self.rawValue.makeData().withUnsafeBytes { buffer -> Int64 in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: Int64.self) else {
return 0
}
return bytes.pointee
}
}
init(_ rawValue: EngineMemoryBuffer) {
self.rawValue = rawValue
}
init(_ value: Int64) {
var value = value
self.rawValue = EngineMemoryBuffer(data: Data(bytes: &value, count: MemoryLayout.size(ofValue: value)))
}
init(_ value: UInt64) {
var value = Int64(bitPattern: value)
self.rawValue = EngineMemoryBuffer(data: Data(bytes: &value, count: MemoryLayout.size(ofValue: value)))
}
}
public func addStoryDraft(engine: TelegramEngine, item: MediaEditorDraft) {
let itemId = MediaEditorDraftItemId(item.path.persistentHashValue)
let _ = engine.orderedLists.addOrMoveToFirstPosition(collectionId: ApplicationSpecificOrderedItemListCollectionId.storyDrafts, id: itemId.rawValue, item: item, removeTailIfCountExceeds: 50).start()
}
public func removeStoryDraft(engine: TelegramEngine, path: String, delete: Bool) {
if delete {
try? FileManager.default.removeItem(atPath: fullDraftPath(peerId: engine.account.peerId, path: path))
}
let itemId = MediaEditorDraftItemId(path.persistentHashValue)
let _ = engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.storyDrafts, id: itemId.rawValue).start()
}
public func clearStoryDrafts(engine: TelegramEngine) {
let _ = engine.data.get(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: ApplicationSpecificOrderedItemListCollectionId.storyDrafts)).start(next: { items in
for item in items {
if let draft = item.contents.get(MediaEditorDraft.self) {
removeStoryDraft(engine: engine, path: draft.path, delete: true)
}
}
})
}
public func deleteAllStoryDrafts(peerId: EnginePeer.Id) {
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/storyDrafts_\(peerId.toInt64())/"
try? FileManager.default.removeItem(atPath: path)
}
public func storyDrafts(engine: TelegramEngine) -> Signal<[MediaEditorDraft], NoError> {
return engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: ApplicationSpecificOrderedItemListCollectionId.storyDrafts))
|> map { items -> [MediaEditorDraft] in
var result: [MediaEditorDraft] = []
for item in items {
if let draft = item.contents.get(MediaEditorDraft.self) {
result.append(draft)
}
}
return result
}
}
public func updateStoryDrafts(engine: TelegramEngine) {
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let _ = engine.data.get(
TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: ApplicationSpecificOrderedItemListCollectionId.storyDrafts)
).start(next: { items in
for item in items {
if let draft = item.contents.get(MediaEditorDraft.self) {
if let expiresOn = draft.expiresOn, expiresOn < currentTimestamp {
removeStoryDraft(engine: engine, path: draft.path, delete: true)
}
}
}
})
}
public extension MediaEditorDraft {
func fullPath(engine: TelegramEngine) -> String {
return fullDraftPath(peerId: engine.account.peerId, path: self.path)
}
}
func fullDraftPath(peerId: EnginePeer.Id, path: String) -> String {
return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/storyDrafts_\(peerId.toInt64())/" + path
}