From 5bf4a36d46b5982db9f31adde9008b696daeccdb Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 10 Aug 2023 17:34:56 +0200 Subject: [PATCH] Various fixes --- .../MediaEditor/Sources/MediaEditor.swift | 10 ++- .../Sources/MediaEditorDraft.swift | 46 ++++++++-- .../Sources/MediaEditorScreen.swift | 53 +++++++---- .../Sources/StorySource.swift | 87 +++++++++++++++---- .../TelegramUI/Sources/AppDelegate.swift | 2 + .../Sources/PostboxKeys.swift | 2 + 6 files changed, 155 insertions(+), 45 deletions(-) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 3eadbfe9f8..9b2776f686 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -9,6 +9,7 @@ import Display import TelegramCore import TelegramPresentationData import FastBlur +import AccountContext public struct MediaEditorPlayerState { public let generationTimestamp: Double @@ -41,6 +42,7 @@ public final class MediaEditor { } } + private let context: AccountContext private let subject: Subject private var player: AVPlayer? private var additionalPlayer: AVPlayer? @@ -253,7 +255,8 @@ public final class MediaEditor { } } - public init(subject: Subject, values: MediaEditorValues? = nil, hasHistogram: Bool = false) { + public init(context: AccountContext, subject: Subject, values: MediaEditorValues? = nil, hasHistogram: Bool = false) { + self.context = context self.subject = subject if let values { self.values = values @@ -332,6 +335,7 @@ public final class MediaEditor { print("error") } + let context = self.context let textureSource: Signal<(TextureSource, UIImage?, AVPlayer?, AVPlayer?, UIColor, UIColor), NoError> switch subject { case let .image(image, _): @@ -340,7 +344,7 @@ public final class MediaEditor { case let .draft(draft): if draft.isVideo { textureSource = Signal { subscriber in - let url = URL(fileURLWithPath: draft.fullPath()) + let url = URL(fileURLWithPath: draft.fullPath(engine: context.engine)) let asset = AVURLAsset(url: url) let playerItem = AVPlayerItem(asset: asset) @@ -372,7 +376,7 @@ public final class MediaEditor { } } } else { - guard let image = UIImage(contentsOfFile: draft.fullPath()) else { + guard let image = UIImage(contentsOfFile: draft.fullPath(engine: context.engine)) else { return } let colors: (UIColor, UIColor) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift index 9672908567..ed938a99e1 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift @@ -70,6 +70,7 @@ public final class MediaEditorDraft: Codable, Equatable { case timestamp case locationLatitude case locationLongitude + case expiresOn } public let path: String @@ -82,8 +83,9 @@ public final class MediaEditorDraft: Codable, Equatable { public let privacy: MediaEditorResultPrivacy? 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?, timestamp: Int32, location: CLLocationCoordinate2D?) { + public init(path: String, isVideo: Bool, thumbnail: UIImage, dimensions: PixelDimensions, duration: Double?, values: MediaEditorValues, caption: NSAttributedString, privacy: MediaEditorResultPrivacy?, timestamp: Int32, location: CLLocationCoordinate2D?, expiresOn: Int32?) { self.path = path self.isVideo = isVideo self.thumbnail = thumbnail @@ -94,6 +96,7 @@ public final class MediaEditorDraft: Codable, Equatable { self.privacy = privacy self.timestamp = timestamp self.location = location + self.expiresOn = expiresOn } public init(from decoder: Decoder) throws { @@ -133,6 +136,8 @@ public final class MediaEditorDraft: Codable, Equatable { } else { self.location = nil } + + self.expiresOn = try container.decodeIfPresent(Int32.self, forKey: .expiresOn) } public func encode(to encoder: Encoder) throws { @@ -170,6 +175,7 @@ public final class MediaEditorDraft: Codable, Equatable { try container.encodeNil(forKey: .locationLatitude) try container.encodeNil(forKey: .locationLongitude) } + try container.encodeIfPresent(self.expiresOn, forKey: .expiresOn) } } @@ -207,14 +213,25 @@ public func addStoryDraft(engine: TelegramEngine, item: MediaEditorDraft) { public func removeStoryDraft(engine: TelegramEngine, path: String, delete: Bool) { if delete { - try? FileManager.default.removeItem(atPath: fullDraftPath(path)) + try? FileManager.default.removeItem(atPath: fullDraftPath(engine: engine, 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.orderedLists.clear(collectionId: ApplicationSpecificOrderedItemListCollectionId.storyDrafts).start() + 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> { @@ -230,12 +247,27 @@ public func storyDrafts(engine: TelegramEngine) -> Signal<[MediaEditorDraft], No } } +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() -> String { - return fullDraftPath(self.path) + func fullPath(engine: TelegramEngine) -> String { + return fullDraftPath(engine: engine, path: self.path) } } -private func fullDraftPath(_ path: String) -> String { - return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/storyDrafts/" + path +private func fullDraftPath(engine: TelegramEngine, path: String) -> String { + return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/storyDrafts_\(engine.account.peerId.toInt64())/" + path } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index ce8453558a..9ed314cd98 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1886,7 +1886,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - let mediaEditor = MediaEditor(subject: subject.editorSubject, values: initialValues, hasHistogram: true) + let mediaEditor = MediaEditor(context: self.context, subject: subject.editorSubject, values: initialValues, hasHistogram: true) if let initialVideoPosition = self.controller?.initialVideoPosition { mediaEditor.seek(initialVideoPosition, andPlay: true) } @@ -3441,6 +3441,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } }) } + + updateStorySources(engine: self.context.engine) + updateStoryDrafts(engine: self.context.engine) } required public init(coder aDecoder: NSCoder) { @@ -3827,23 +3830,35 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate guard let subject = self.node.subject, let mediaEditor = self.node.mediaEditor else { return } - try? FileManager.default.createDirectory(atPath: draftPath(), withIntermediateDirectories: true) + try? FileManager.default.createDirectory(atPath: draftPath(engine: self.context.engine), withIntermediateDirectories: true) let values = mediaEditor.values let privacy = self.state.privacy let caption = self.getCaption() let duration = mediaEditor.duration ?? 0.0 + let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) var timestamp: Int32 var location: CLLocationCoordinate2D? + let expiresOn: Int32 if case let .draft(draft, _) = subject { timestamp = draft.timestamp location = draft.location + if let _ = id { + expiresOn = draft.expiresOn ?? currentTimestamp + 3600 * 24 * 7 + } else { + expiresOn = currentTimestamp + 3600 * 24 * 7 + } } else { - timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + timestamp = currentTimestamp if case let .asset(asset) = subject { location = asset.location?.coordinate } + if let _ = id { + expiresOn = currentTimestamp + Int32(self.state.privacy.timeout) + } else { + expiresOn = currentTimestamp + 3600 * 24 * 7 + } } if let resultImage = mediaEditor.resultImage { @@ -3859,8 +3874,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) { let path = "\(Int64.random(in: .min ... .max)).jpg" if let data = image.jpegData(compressionQuality: 0.87) { - let draft = MediaEditorDraft(path: path, isVideo: false, thumbnail: thumbnailImage, dimensions: dimensions, duration: nil, values: values, caption: caption, privacy: privacy, timestamp: timestamp, location: location) - try? data.write(to: URL(fileURLWithPath: draft.fullPath())) + let draft = MediaEditorDraft(path: path, isVideo: false, thumbnail: thumbnailImage, dimensions: dimensions, duration: nil, values: values, caption: caption, privacy: privacy, timestamp: timestamp, location: location, expiresOn: expiresOn) + try? data.write(to: URL(fileURLWithPath: draft.fullPath(engine: context.engine))) if let id { saveStorySource(engine: context.engine, item: draft, peerId: context.account.peerId, id: id) } else { @@ -3873,8 +3888,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let saveVideoDraft: (String, PixelDimensions, Double) -> Void = { videoPath, dimensions, duration in if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) { let path = "\(Int64.random(in: .min ... .max)).mp4" - let draft = MediaEditorDraft(path: path, isVideo: true, thumbnail: thumbnailImage, dimensions: dimensions, duration: duration, values: values, caption: caption, privacy: privacy, timestamp: timestamp, location: location) - try? FileManager.default.moveItem(atPath: videoPath, toPath: draft.fullPath()) + let draft = MediaEditorDraft(path: path, isVideo: true, thumbnail: thumbnailImage, dimensions: dimensions, duration: duration, values: values, caption: caption, privacy: privacy, timestamp: timestamp, location: location, expiresOn: expiresOn) + try? FileManager.default.moveItem(atPath: videoPath, toPath: draft.fullPath(engine: context.engine)) if let id { saveStorySource(engine: context.engine, item: draft, peerId: context.account.peerId, id: id) } else { @@ -3906,8 +3921,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } case let .draft(draft, _): if draft.isVideo { - saveVideoDraft(draft.fullPath(), draft.dimensions, draft.duration ?? 0.0) - } else if let image = UIImage(contentsOfFile: draft.fullPath()) { + saveVideoDraft(draft.fullPath(engine: context.engine), draft.dimensions, draft.duration ?? 0.0) + } else if let image = UIImage(contentsOfFile: draft.fullPath(engine: context.engine)) { saveImageDraft(image, draft.dimensions) } removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false) @@ -3943,6 +3958,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate mediaEditor.invalidate() self.node.entitiesView.invalidate() + let context = self.context if let navigationController = self.navigationController as? NavigationController { navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) } @@ -4109,15 +4125,16 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return EmptyDisposable } case let .draft(draft, _): + let draftPath = draft.fullPath(engine: context.engine) if draft.isVideo { - videoResult = .videoFile(path: draft.fullPath()) + videoResult = .videoFile(path: draftPath) if let videoTrimRange = mediaEditor.values.videoTrimRange { duration = videoTrimRange.upperBound - videoTrimRange.lowerBound } else { duration = min(draft.duration ?? 5.0, storyMaxVideoDuration) } firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in - let avAsset = AVURLAsset(url: URL(fileURLWithPath: draft.fullPath())) + let avAsset = AVURLAsset(url: URL(fileURLWithPath: draftPath)) let avAssetGenerator = AVAssetImageGenerator(asset: avAsset) avAssetGenerator.appliesPreferredTrackTransform = true avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in @@ -4131,10 +4148,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } } else { - videoResult = .imageFile(path: draft.fullPath()) + videoResult = .imageFile(path: draftPath) duration = 5.0 - if let image = UIImage(contentsOfFile: draft.fullPath()) { + if let image = UIImage(contentsOfFile: draftPath) { firstFrame = .single((image, nil)) } else { firstFrame = .single((UIImage(), nil)) @@ -4228,6 +4245,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return } + let context = self.context + let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) } let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) @@ -4292,10 +4311,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } case let .draft(draft, _): if draft.isVideo { - let asset = AVURLAsset(url: NSURL(fileURLWithPath: draft.fullPath()) as URL) + let asset = AVURLAsset(url: NSURL(fileURLWithPath: draft.fullPath(engine: context.engine)) as URL) exportSubject = .single(.video(asset)) } else { - if let image = UIImage(contentsOfFile: draft.fullPath()) { + if let image = UIImage(contentsOfFile: draft.fullPath(engine: context.engine)) { exportSubject = .single(.image(image)) } else { fatalError() @@ -4768,8 +4787,8 @@ public final class BlurredGradientComponent: Component { } } -func draftPath() -> String { - return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/storyDrafts" +func draftPath(engine: TelegramEngine) -> String { + return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/storyDrafts_\(engine.account.peerId.toInt64())" } func hasFirstResponder(_ view: UIView) -> Bool { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StorySource.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StorySource.swift index eecf938e89..bc7c5b5fdc 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StorySource.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StorySource.swift @@ -6,24 +6,79 @@ import TelegramUIPreferences import MediaEditor import AccountContext -public func saveStorySource(engine: TelegramEngine, item: MediaEditorDraft, peerId: EnginePeer.Id, id: Int64) { +public func updateStorySources(engine: TelegramEngine) { + let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + let _ = engine.data.get( + TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: ApplicationSpecificOrderedItemListCollectionId.storySources) + ).start(next: { items in + for item in items { + let key = EngineDataBuffer(item.id) + let _ = getStorySource(engine: engine, key: key).start(next: { source in + if let source { + if let expiresOn = source.expiresOn, expiresOn < currentTimestamp { + let _ = removeStorySource(engine: engine, key: key, delete: true).start() + } + } + }) + + } + }) +} + +private func key(peerId: EnginePeer.Id, id: Int64) -> EngineDataBuffer { let key = EngineDataBuffer(length: 16) key.setInt64(0, value: peerId.toInt64()) key.setInt64(8, value: id) + return key +} + +private class StorySourceItem: Codable { +} + +private func addStorySource(engine: TelegramEngine, key: EngineDataBuffer) { + let _ = engine.orderedLists.addOrMoveToFirstPosition( + collectionId: ApplicationSpecificOrderedItemListCollectionId.storySources, + id: key.toMemoryBuffer(), + item: StorySourceItem(), + removeTailIfCountExceeds: nil + ).start() +} + +private func removeStorySource(engine: TelegramEngine, peerId: EnginePeer.Id, id: Int64, delete: Bool) -> Signal { + let key = key(peerId: peerId, id: id) + return getStorySource(engine: engine, peerId: peerId, id: id) + |> mapToSignal { source in + if let source { + let _ = engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: key).start() + removeStoryDraft(engine: engine, path: source.path, delete: delete) + } + return engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.storySources, id: key.toMemoryBuffer()) + } +} + +private func removeStorySource(engine: TelegramEngine, key: EngineDataBuffer, delete: Bool) -> Signal { + return getStorySource(engine: engine, key: key) + |> mapToSignal { source in + if let source { + let _ = engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: key).start() + removeStoryDraft(engine: engine, path: source.path, delete: delete) + } + return engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.storySources, id: key.toMemoryBuffer()) + } +} + +public func saveStorySource(engine: TelegramEngine, item: MediaEditorDraft, peerId: EnginePeer.Id, id: Int64) { + let key = key(peerId: peerId, id: id) + addStorySource(engine: engine, key: key) let _ = engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: key, item: item).start() } -public func removeStorySource(engine: TelegramEngine, peerId: EnginePeer.Id, id: Int64) { - let key = EngineDataBuffer(length: 16) - key.setInt64(0, value: peerId.toInt64()) - key.setInt64(8, value: id) - let _ = engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: key).start() +public func getStorySource(engine: TelegramEngine, peerId: EnginePeer.Id, id: Int64) -> Signal { + let key = key(peerId: peerId, id: id) + return getStorySource(engine: engine, key: key) } -public func getStorySource(engine: TelegramEngine, peerId: EnginePeer.Id, id: Int64) -> Signal { - let key = EngineDataBuffer(length: 16) - key.setInt64(0, value: peerId.toInt64()) - key.setInt64(8, value: id) +private func getStorySource(engine: TelegramEngine, key: EngineDataBuffer) -> Signal { return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: key)) |> map { result -> MediaEditorDraft? in return result?.get(MediaEditorDraft.self) @@ -31,20 +86,16 @@ public func getStorySource(engine: TelegramEngine, peerId: EnginePeer.Id, id: In } public func moveStorySource(engine: TelegramEngine, peerId: EnginePeer.Id, from fromId: Int64, to toId: Int64) { - let fromKey = EngineDataBuffer(length: 16) - fromKey.setInt64(0, value: peerId.toInt64()) - fromKey.setInt64(8, value: fromId) - - let toKey = EngineDataBuffer(length: 16) - toKey.setInt64(0, value: peerId.toInt64()) - toKey.setInt64(8, value: toId) + let fromKey = key(peerId: peerId, id: fromId) + let toKey = key(peerId: peerId, id: toId) let _ = (engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: fromKey)) |> mapToSignal { item -> Signal in if let item = item?.get(MediaEditorDraft.self) { + addStorySource(engine: engine, key: toKey) return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: toKey, item: item) |> then( - engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: fromKey) + removeStorySource(engine: engine, key: fromKey, delete: false) ) } else { return .complete() diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 72cb7ec47f..421bd18988 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -38,6 +38,7 @@ import PhoneNumberFormat import AuthorizationUI import ManagedFile import DeviceProximity +import MediaEditor #if canImport(AppCenter) import AppCenter @@ -1291,6 +1292,7 @@ private func extractAccountManagerState(records: AccountRecordsView