mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various fixes
This commit is contained in:
parent
1ed853e255
commit
9e0600edfa
@ -14290,3 +14290,6 @@ Sorry for the inconvenience.";
|
||||
"MediaPicker.CreateStory_1" = "Create %@ Story";
|
||||
"MediaPicker.CreateStory_any" = "Create %@ Stories";
|
||||
"MediaPicker.CombineIntoCollage" = "Combine into Collage";
|
||||
|
||||
"Gift.Resale.Unavailable.Title" = "Resell Gift";
|
||||
"Gift.Resale.Unavailable.Text" = "Sorry, you can't list this gift yet.\n\Reselling will be available on %@.";
|
||||
|
@ -2004,7 +2004,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
||||
var hasSelect = false
|
||||
if forCollage {
|
||||
hasSelect = true
|
||||
} else if case .story = mode {
|
||||
} else if case .story = mode, selectionContext.selectionLimit > 1 {
|
||||
hasSelect = true
|
||||
}
|
||||
|
||||
@ -3402,7 +3402,7 @@ public func stickerMediaPickerController(
|
||||
destinationCornerRadius: 0.0
|
||||
)
|
||||
},
|
||||
completion: { result, _, commit in
|
||||
completion: { result, _, _, commit in
|
||||
completion(result, nil, .zero, nil, true, { _ in return nil }, {
|
||||
returnToCameraImpl?()
|
||||
})
|
||||
@ -3520,7 +3520,7 @@ public func avatarMediaPickerController(
|
||||
destinationCornerRadius: 0.0
|
||||
)
|
||||
},
|
||||
completion: { result, _, commit in
|
||||
completion: { result, _, _, commit in
|
||||
completion(result, nil, .zero, nil, true, { _ in return nil }, {
|
||||
returnToCameraImpl?()
|
||||
})
|
||||
|
@ -1959,6 +1959,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
}
|
||||
},
|
||||
nil,
|
||||
1,
|
||||
{}
|
||||
)
|
||||
} else {
|
||||
@ -1995,6 +1996,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
}
|
||||
},
|
||||
nil,
|
||||
self.controller?.remainingStoryCount,
|
||||
{}
|
||||
)
|
||||
}
|
||||
@ -3374,7 +3376,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
self.transitionOut = transitionOut
|
||||
}
|
||||
}
|
||||
fileprivate let completion: (Signal<CameraScreenImpl.Result, NoError>, ResultTransition?, @escaping () -> Void) -> Void
|
||||
fileprivate let completion: (Signal<CameraScreenImpl.Result, NoError>, ResultTransition?, Int32?, @escaping () -> Void) -> Void
|
||||
public var transitionedIn: () -> Void = {}
|
||||
public var transitionedOut: () -> Void = {}
|
||||
|
||||
@ -3382,6 +3384,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
|
||||
private let postingAvailabilityPromise = Promise<StoriesUploadAvailability>()
|
||||
private var postingAvailabilityDisposable: Disposable?
|
||||
private var remainingStoryCount: Int32?
|
||||
|
||||
private var codeDisposable: Disposable?
|
||||
private var resolveCodeDisposable: Disposable?
|
||||
@ -3419,7 +3422,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
holder: CameraHolder? = nil,
|
||||
transitionIn: TransitionIn?,
|
||||
transitionOut: @escaping (Bool) -> TransitionOut?,
|
||||
completion: @escaping (Signal<CameraScreenImpl.Result, NoError>, ResultTransition?, @escaping () -> Void) -> Void
|
||||
completion: @escaping (Signal<CameraScreenImpl.Result, NoError>, ResultTransition?, Int32?, @escaping () -> Void) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
@ -3473,7 +3476,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
return
|
||||
}
|
||||
if case let .available(remainingCount) = availability {
|
||||
let _ = remainingCount
|
||||
self.remainingStoryCount = remainingCount
|
||||
return
|
||||
}
|
||||
self.node.postingAvailable = false
|
||||
@ -3638,10 +3641,14 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
} else {
|
||||
if self.cameraState.isCollageEnabled {
|
||||
selectionLimit = 6
|
||||
} else {
|
||||
if let remainingStoryCount = self.remainingStoryCount {
|
||||
selectionLimit = min(Int(remainingStoryCount), 10)
|
||||
} else {
|
||||
selectionLimit = 10
|
||||
}
|
||||
}
|
||||
}
|
||||
controller = self.context.sharedContext.makeStoryMediaPickerScreen(
|
||||
context: self.context,
|
||||
isDark: true,
|
||||
@ -3704,10 +3711,10 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
)
|
||||
self.present(alertController, in: .window(.root))
|
||||
} else {
|
||||
self.completion(.single(.asset(asset)), resultTransition, dismissed)
|
||||
self.completion(.single(.asset(asset)), resultTransition, self.remainingStoryCount, dismissed)
|
||||
}
|
||||
} else if let draft = result as? MediaEditorDraft {
|
||||
self.completion(.single(.draft(draft)), resultTransition, dismissed)
|
||||
self.completion(.single(.draft(draft)), resultTransition, self.remainingStoryCount, dismissed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3753,7 +3760,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
}
|
||||
} else {
|
||||
if let assets = results as? [PHAsset] {
|
||||
self.completion(.single(.assets(assets)), nil, {
|
||||
self.completion(.single(.assets(assets)), nil, self.remainingStoryCount, {
|
||||
|
||||
})
|
||||
}
|
||||
|
@ -338,8 +338,6 @@ final class GiftStoreScreenComponent: Component {
|
||||
)
|
||||
|
||||
if let effectiveGifts = self.effectiveGifts, effectiveGifts.isEmpty && self.state?.starGiftsState?.dataState != .loading {
|
||||
showClearFilters = true
|
||||
|
||||
let emptyAnimationHeight = 148.0
|
||||
let visibleHeight = availableHeight
|
||||
let emptyAnimationSpacing: CGFloat = 20.0
|
||||
|
@ -2820,7 +2820,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
case upgradePreview([StarGift.UniqueGift.Attribute], String)
|
||||
case wearPreview(StarGift.UniqueGift)
|
||||
|
||||
var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellStars: Int64?, canExportDate: Int32?, upgradeMessageId: Int32?)? {
|
||||
var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellStars: Int64?, canExportDate: Int32?, upgradeMessageId: Int32?, canTransferDate: Int32?, canResaleDate: Int32?)? {
|
||||
switch self {
|
||||
case let .message(message):
|
||||
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction {
|
||||
@ -2832,8 +2832,8 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
} else {
|
||||
reference = .message(messageId: message.id)
|
||||
}
|
||||
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId)
|
||||
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, peerId, senderId, savedId, _, _, _):
|
||||
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil)
|
||||
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate):
|
||||
var reference: StarGiftReference
|
||||
if let peerId, let savedId {
|
||||
reference = .peer(peerId: peerId, id: savedId)
|
||||
@ -2857,13 +2857,13 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
if case let .unique(uniqueGift) = gift {
|
||||
resellStars = uniqueGift.resellStars
|
||||
}
|
||||
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellStars, canExportDate, nil)
|
||||
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellStars, canExportDate, nil, canTransferDate, canResaleDate)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
case let .uniqueGift(gift, _), let .wearPreview(gift):
|
||||
return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellStars, nil, nil)
|
||||
return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellStars, nil, nil, nil, nil)
|
||||
case let .profileGift(peerId, gift):
|
||||
var messageId: EngineMessage.Id?
|
||||
if case let .message(messageIdValue) = gift.reference {
|
||||
@ -2873,7 +2873,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
if case let .unique(uniqueGift) = gift.gift {
|
||||
resellStars = uniqueGift.resellStars
|
||||
}
|
||||
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellStars, gift.canExportDate, nil)
|
||||
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellStars, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate)
|
||||
case .soldOutGift:
|
||||
return nil
|
||||
case .upgradePreview:
|
||||
@ -3400,6 +3400,22 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
|
||||
self.dismissAllTooltips()
|
||||
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
if let canResaleDate = arguments.canResaleDate, currentTime < canResaleDate {
|
||||
let dateString = stringForFullDate(timestamp: canResaleDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
|
||||
let controller = textAlertController(
|
||||
context: self.context,
|
||||
title: presentationData.strings.Gift_Resale_Unavailable_Title,
|
||||
text: presentationData.strings.Gift_Resale_Unavailable_Text(dateString).string,
|
||||
actions: [
|
||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})
|
||||
],
|
||||
parseMarkdown: true
|
||||
)
|
||||
self.present(controller, in: .window(.root))
|
||||
return
|
||||
}
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))"
|
||||
let reference = arguments.reference ?? .slug(slug: gift.slug)
|
||||
@ -3582,7 +3598,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
|
||||
if case let .unique(gift) = arguments.gift, let resellStars = gift.resellStars, resellStars > 0 {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_ChangePrice, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Paid"), color: theme.contextMenu.primaryColor)
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/PriceTag"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { c, _ in
|
||||
c?.dismiss(completion: nil)
|
||||
|
||||
|
@ -988,6 +988,8 @@ public final class MediaEditor {
|
||||
if let trimRange = self.values.videoTrimRange {
|
||||
player.currentItem?.forwardPlaybackEndTime = CMTime(seconds: trimRange.upperBound, preferredTimescale: CMTimeScale(1000))
|
||||
// additionalPlayer?.currentItem?.forwardPlaybackEndTime = CMTime(seconds: trimRange.upperBound, preferredTimescale: CMTimeScale(1000))
|
||||
} else if let duration = player.currentItem?.duration.seconds, duration > self.maxDuration {
|
||||
player.currentItem?.forwardPlaybackEndTime = CMTime(seconds: self.maxDuration, preferredTimescale: CMTimeScale(1000))
|
||||
}
|
||||
|
||||
if let initialSeekPosition = self.initialSeekPosition {
|
||||
|
@ -97,7 +97,7 @@ public extension MediaEditorScreenImpl {
|
||||
var updateProgressImpl: ((Float) -> Void)?
|
||||
let controller = MediaEditorScreenImpl(
|
||||
context: context,
|
||||
mode: .storyEditor,
|
||||
mode: .storyEditor(remainingCount: 1),
|
||||
subject: subject,
|
||||
isEditing: !repost,
|
||||
isEditingCover: cover,
|
||||
|
@ -2019,13 +2019,17 @@ final class MediaEditorScreenComponent: Component {
|
||||
} else {
|
||||
minDuration = 1.0
|
||||
if case .avatarEditor = controller.mode {
|
||||
maxDuration = 9.9
|
||||
maxDuration = avatarMaxVideoDuration
|
||||
} else {
|
||||
if controller.node.items.count > 0 {
|
||||
maxDuration = storyMaxVideoDuration
|
||||
} else {
|
||||
maxDuration = storyMaxCombinedVideoDuration
|
||||
if case let .storyEditor(remainingCount) = controller.mode, remainingCount > 1 {
|
||||
maxDuration = min(storyMaxCombinedVideoDuration, Double(remainingCount) * storyMaxVideoDuration)
|
||||
segmentDuration = storyMaxVideoDuration
|
||||
} else {
|
||||
maxDuration = storyMaxVideoDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2843,6 +2847,8 @@ let storyMaxVideoDuration: Double = 60.0
|
||||
let storyMaxCombinedVideoCount: Int = 3
|
||||
let storyMaxCombinedVideoDuration: Double = storyMaxVideoDuration * Double(storyMaxCombinedVideoCount)
|
||||
|
||||
let avatarMaxVideoDuration: Double = 10.0
|
||||
|
||||
public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UIDropInteractionDelegate {
|
||||
public enum Mode {
|
||||
public enum StickerEditorMode {
|
||||
@ -2852,7 +2858,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
case businessIntro
|
||||
}
|
||||
|
||||
case storyEditor
|
||||
case storyEditor(remainingCount: Int32)
|
||||
case stickerEditor(mode: StickerEditorMode)
|
||||
case botPreview
|
||||
case avatarEditor
|
||||
@ -3510,8 +3516,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
values: initialValues,
|
||||
hasHistogram: true
|
||||
)
|
||||
if case .storyEditor = controller.mode, self.items.isEmpty {
|
||||
mediaEditor.maxDuration = storyMaxCombinedVideoDuration
|
||||
if case let .storyEditor(remainingCount) = controller.mode, self.items.isEmpty {
|
||||
mediaEditor.maxDuration = min(storyMaxCombinedVideoDuration, Double(remainingCount) * storyMaxVideoDuration)
|
||||
} else if case .avatarEditor = controller.mode {
|
||||
mediaEditor.maxDuration = avatarMaxVideoDuration
|
||||
}
|
||||
|
||||
if case .avatarEditor = controller.mode {
|
||||
@ -6549,15 +6557,17 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
fileprivate let customTarget: EnginePeer.Id?
|
||||
let forwardSource: (EnginePeer, EngineStoryItem)?
|
||||
|
||||
fileprivate let initialCaption: NSAttributedString?
|
||||
fileprivate let initialPrivacy: EngineStoryPrivacy?
|
||||
fileprivate let initialMediaAreas: [MediaArea]?
|
||||
fileprivate let initialVideoPosition: Double?
|
||||
fileprivate let initialLink: (url: String, name: String?)?
|
||||
let initialCaption: NSAttributedString?
|
||||
let initialPrivacy: EngineStoryPrivacy?
|
||||
let initialMediaAreas: [MediaArea]?
|
||||
let initialVideoPosition: Double?
|
||||
let initialLink: (url: String, name: String?)?
|
||||
|
||||
fileprivate let transitionIn: TransitionIn?
|
||||
fileprivate let transitionOut: (Bool, Bool?) -> TransitionOut?
|
||||
|
||||
var didComplete = false
|
||||
|
||||
public var cancelled: (Bool) -> Void = { _ in }
|
||||
public var willComplete: (UIImage?, Bool, @escaping () -> Void) -> Void
|
||||
public var completion: ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
@ -6784,7 +6794,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var isEmbeddedEditor: Bool {
|
||||
var isEmbeddedEditor: Bool {
|
||||
return self.isEditingStory || self.isEditingStoryCover || self.forwardSource != nil
|
||||
}
|
||||
|
||||
@ -7386,825 +7396,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
return true
|
||||
}
|
||||
|
||||
private func processMultipleItems(items: [EditingItem], isLongVideo: Bool) {
|
||||
guard !items.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
var items = items
|
||||
if let mediaEditor = self.node.mediaEditor, case let .asset(asset) = self.node.subject, let currentItemIndex = items.firstIndex(where: { $0.asset.localIdentifier == asset.localIdentifier }) {
|
||||
var updatedCurrentItem = items[currentItemIndex]
|
||||
updatedCurrentItem.caption = self.node.getCaption()
|
||||
updatedCurrentItem.values = mediaEditor.values
|
||||
items[currentItemIndex] = updatedCurrentItem
|
||||
}
|
||||
|
||||
let multipleResults = Atomic<[MediaEditorScreenImpl.Result]>(value: [])
|
||||
let totalItems = items.count
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
let privacy = self.state.privacy
|
||||
|
||||
if !(self.isEditingStory || self.isEditingStoryCover) {
|
||||
let _ = updateMediaEditorStoredStateInteractively(engine: self.context.engine, { current in
|
||||
if let current {
|
||||
return current.withUpdatedPrivacy(privacy)
|
||||
} else {
|
||||
return MediaEditorStoredState(privacy: privacy, textSettings: nil)
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
|
||||
var order: [Int64] = []
|
||||
for (index, item) in items.enumerated() {
|
||||
guard item.isEnabled else {
|
||||
continue
|
||||
}
|
||||
|
||||
dispatchGroup.enter()
|
||||
|
||||
let randomId = Int64.random(in: .min ... .max)
|
||||
order.append(randomId)
|
||||
|
||||
if item.asset.mediaType == .video {
|
||||
processVideoItem(item: item, index: index, randomId: randomId, isLongVideo: isLongVideo) { result in
|
||||
let _ = multipleResults.modify { results in
|
||||
var updatedResults = results
|
||||
updatedResults.append(result)
|
||||
return updatedResults
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
} else if item.asset.mediaType == .image {
|
||||
processImageItem(item: item, index: index, randomId: randomId) { result in
|
||||
let _ = multipleResults.modify { results in
|
||||
var updatedResults = results
|
||||
updatedResults.append(result)
|
||||
return updatedResults
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
} else {
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
let results = multipleResults.with { $0 }
|
||||
if results.count == totalItems {
|
||||
var orderedResults: [MediaEditorScreenImpl.Result] = []
|
||||
for id in order {
|
||||
if let item = results.first(where: { $0.randomId == id }) {
|
||||
orderedResults.append(item)
|
||||
}
|
||||
}
|
||||
self.completion(results, { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
finished()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func processVideoItem(item: EditingItem, index: Int, randomId: Int64, isLongVideo: Bool, completion: @escaping (MediaEditorScreenImpl.Result) -> Void) {
|
||||
let asset = item.asset
|
||||
|
||||
let itemMediaEditor = setupMediaEditorForItem(item: item)
|
||||
|
||||
var caption = item.caption
|
||||
caption = convertMarkdownToAttributes(caption)
|
||||
|
||||
var mediaAreas: [MediaArea] = []
|
||||
var stickers: [TelegramMediaFile] = []
|
||||
|
||||
if let entities = item.values?.entities {
|
||||
for entity in entities {
|
||||
if let mediaArea = entity.mediaArea {
|
||||
mediaAreas.append(mediaArea)
|
||||
}
|
||||
extractStickersFromEntity(entity, into: &stickers)
|
||||
}
|
||||
}
|
||||
|
||||
let firstFrameTime: CMTime
|
||||
if let coverImageTimestamp = item.values?.coverImageTimestamp, !isLongVideo || index == 0 {
|
||||
firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60))
|
||||
} else {
|
||||
firstFrameTime = CMTime(seconds: item.values?.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60))
|
||||
}
|
||||
|
||||
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { [weak self] avAsset, _, _ in
|
||||
guard let avAsset else {
|
||||
DispatchQueue.main.async {
|
||||
if let self {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let duration: Double
|
||||
if let videoTrimRange = item.values?.videoTrimRange {
|
||||
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
||||
} else {
|
||||
duration = min(asset.duration, storyMaxVideoDuration)
|
||||
}
|
||||
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)]) { [weak self] _, cgImage, _, _, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
if let cgImage {
|
||||
let image = UIImage(cgImage: cgImage)
|
||||
itemMediaEditor.replaceSource(image, additionalImage: nil, time: firstFrameTime, mirror: false)
|
||||
|
||||
if let resultImage = itemMediaEditor.resultImage {
|
||||
makeEditorImageComposition(
|
||||
context: self.node.ciContext,
|
||||
postbox: self.context.account.postbox,
|
||||
inputImage: resultImage,
|
||||
dimensions: storyDimensions,
|
||||
values: itemMediaEditor.values,
|
||||
time: firstFrameTime,
|
||||
textScale: 2.0
|
||||
) { coverImage in
|
||||
if let coverImage = coverImage {
|
||||
let result = MediaEditorScreenImpl.Result(
|
||||
media: .video(
|
||||
video: .asset(localIdentifier: asset.localIdentifier),
|
||||
coverImage: coverImage,
|
||||
values: itemMediaEditor.values,
|
||||
duration: duration,
|
||||
dimensions: itemMediaEditor.values.resultDimensions
|
||||
),
|
||||
mediaAreas: mediaAreas,
|
||||
caption: caption,
|
||||
coverTimestamp: itemMediaEditor.values.coverImageTimestamp,
|
||||
options: self.state.privacy,
|
||||
stickers: stickers,
|
||||
randomId: randomId
|
||||
)
|
||||
completion(result)
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func processImageItem(item: EditingItem, index: Int, randomId: Int64, completion: @escaping (MediaEditorScreenImpl.Result) -> Void) {
|
||||
let asset = item.asset
|
||||
|
||||
let itemMediaEditor = setupMediaEditorForItem(item: item)
|
||||
|
||||
var caption = item.caption
|
||||
caption = convertMarkdownToAttributes(caption)
|
||||
|
||||
var mediaAreas: [MediaArea] = []
|
||||
var stickers: [TelegramMediaFile] = []
|
||||
|
||||
if let entities = item.values?.entities {
|
||||
for entity in entities {
|
||||
if let mediaArea = entity.mediaArea {
|
||||
mediaAreas.append(mediaArea)
|
||||
}
|
||||
extractStickersFromEntity(entity, into: &stickers)
|
||||
}
|
||||
}
|
||||
|
||||
let options = PHImageRequestOptions()
|
||||
options.deliveryMode = .highQualityFormat
|
||||
options.isNetworkAccessAllowed = true
|
||||
|
||||
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { [weak self] image, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
if let image {
|
||||
itemMediaEditor.replaceSource(image, additionalImage: nil, time: .zero, mirror: false)
|
||||
|
||||
if let resultImage = itemMediaEditor.resultImage {
|
||||
makeEditorImageComposition(
|
||||
context: self.node.ciContext,
|
||||
postbox: self.context.account.postbox,
|
||||
inputImage: resultImage,
|
||||
dimensions: storyDimensions,
|
||||
values: itemMediaEditor.values,
|
||||
time: .zero,
|
||||
textScale: 2.0
|
||||
) { resultImage in
|
||||
if let resultImage = resultImage {
|
||||
let result = MediaEditorScreenImpl.Result(
|
||||
media: .image(
|
||||
image: resultImage,
|
||||
dimensions: PixelDimensions(resultImage.size)
|
||||
),
|
||||
mediaAreas: mediaAreas,
|
||||
caption: caption,
|
||||
coverTimestamp: nil,
|
||||
options: self.state.privacy,
|
||||
stickers: stickers,
|
||||
randomId: randomId
|
||||
)
|
||||
completion(result)
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupMediaEditorForItem(item: EditingItem) -> MediaEditor {
|
||||
var values = item.values
|
||||
if values?.videoTrimRange == nil {
|
||||
values = values?.withUpdatedVideoTrimRange(0 ..< storyMaxVideoDuration)
|
||||
}
|
||||
return MediaEditor(
|
||||
context: self.context,
|
||||
mode: .default,
|
||||
subject: .asset(item.asset),
|
||||
values: values,
|
||||
hasHistogram: false,
|
||||
isStandalone: true
|
||||
)
|
||||
}
|
||||
|
||||
private func extractStickersFromEntity(_ entity: CodableDrawingEntity, into stickers: inout [TelegramMediaFile]) {
|
||||
switch entity {
|
||||
case let .sticker(stickerEntity):
|
||||
if case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
|
||||
stickers.append(file.media)
|
||||
}
|
||||
case let .text(textEntity):
|
||||
if let subEntities = textEntity.renderSubEntities {
|
||||
for entity in subEntities {
|
||||
if let stickerEntity = entity as? DrawingStickerEntity, case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
|
||||
stickers.append(file.media)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func createEmptyResult(randomId: Int64) -> MediaEditorScreenImpl.Result {
|
||||
let emptyImage = UIImage()
|
||||
return MediaEditorScreenImpl.Result(
|
||||
media: .image(
|
||||
image: emptyImage,
|
||||
dimensions: PixelDimensions(emptyImage.size)
|
||||
),
|
||||
mediaAreas: [],
|
||||
caption: NSAttributedString(),
|
||||
coverTimestamp: nil,
|
||||
options: self.state.privacy,
|
||||
stickers: [],
|
||||
randomId: randomId
|
||||
)
|
||||
}
|
||||
|
||||
private func processSingleItem() {
|
||||
guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, let actualSubject = self.node.actualSubject else {
|
||||
return
|
||||
}
|
||||
|
||||
var caption = self.node.getCaption()
|
||||
caption = convertMarkdownToAttributes(caption)
|
||||
|
||||
var hasEntityChanges = false
|
||||
let randomId: Int64
|
||||
if case let .draft(_, id) = actualSubject, let id {
|
||||
randomId = id
|
||||
} else {
|
||||
randomId = Int64.random(in: .min ... .max)
|
||||
}
|
||||
|
||||
let codableEntities = mediaEditor.values.entities
|
||||
var mediaAreas: [MediaArea] = []
|
||||
if case let .draft(draft, _) = actualSubject {
|
||||
if draft.values.entities != codableEntities {
|
||||
hasEntityChanges = true
|
||||
}
|
||||
} else {
|
||||
mediaAreas = self.initialMediaAreas ?? []
|
||||
}
|
||||
|
||||
var stickers: [TelegramMediaFile] = []
|
||||
for entity in codableEntities {
|
||||
switch entity {
|
||||
case let .sticker(stickerEntity):
|
||||
if case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
|
||||
stickers.append(file.media)
|
||||
}
|
||||
case let .text(textEntity):
|
||||
if let subEntities = textEntity.renderSubEntities {
|
||||
for entity in subEntities {
|
||||
if let stickerEntity = entity as? DrawingStickerEntity, case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
|
||||
stickers.append(file.media)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
if let mediaArea = entity.mediaArea {
|
||||
mediaAreas.append(mediaArea)
|
||||
}
|
||||
}
|
||||
|
||||
var hasAnyChanges = self.node.hasAnyChanges
|
||||
if self.isEditingStoryCover {
|
||||
hasAnyChanges = false
|
||||
}
|
||||
|
||||
if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) {
|
||||
self.saveDraft(id: randomId, isEdit: true)
|
||||
|
||||
self.completion([MediaEditorScreenImpl.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
finished()
|
||||
}
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !(self.isEditingStory || self.isEditingStoryCover) {
|
||||
let privacy = self.state.privacy
|
||||
let _ = updateMediaEditorStoredStateInteractively(engine: self.context.engine, { current in
|
||||
if let current {
|
||||
return current.withUpdatedPrivacy(privacy)
|
||||
} else {
|
||||
return MediaEditorStoredState(privacy: privacy, textSettings: nil)
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
|
||||
if mediaEditor.resultIsVideo {
|
||||
self.saveDraft(id: randomId)
|
||||
|
||||
var firstFrame: Signal<(UIImage?, UIImage?), NoError>
|
||||
let firstFrameTime: CMTime
|
||||
if let coverImageTimestamp = mediaEditor.values.coverImageTimestamp {
|
||||
firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60))
|
||||
} else {
|
||||
firstFrameTime = CMTime(seconds: mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60))
|
||||
}
|
||||
let videoResult: Signal<MediaResult.VideoResult, NoError>
|
||||
var videoIsMirrored = false
|
||||
let duration: Double
|
||||
switch subject {
|
||||
case let .empty(dimensions):
|
||||
let image = generateImage(dimensions.cgSize, opaque: false, scale: 1.0, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
})!
|
||||
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
|
||||
if let data = image.jpegData(compressionQuality: 0.85) {
|
||||
try? data.write(to: URL(fileURLWithPath: tempImagePath))
|
||||
}
|
||||
videoResult = .single(.imageFile(path: tempImagePath))
|
||||
duration = 3.0
|
||||
|
||||
firstFrame = .single((image, nil))
|
||||
case let .image(image, _, _, _):
|
||||
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
|
||||
if let data = image.jpegData(compressionQuality: 0.85) {
|
||||
try? data.write(to: URL(fileURLWithPath: tempImagePath))
|
||||
}
|
||||
videoResult = .single(.imageFile(path: tempImagePath))
|
||||
duration = 5.0
|
||||
|
||||
firstFrame = .single((image, nil))
|
||||
case let .video(path, _, mirror, additionalPath, _, _, durationValue, _, _):
|
||||
videoIsMirrored = mirror
|
||||
videoResult = .single(.videoFile(path: path))
|
||||
if let videoTrimRange = mediaEditor.values.videoTrimRange {
|
||||
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
||||
} else {
|
||||
duration = durationValue
|
||||
}
|
||||
|
||||
var additionalPath = additionalPath
|
||||
if additionalPath == nil, let valuesAdditionalPath = mediaEditor.values.additionalVideoPath {
|
||||
additionalPath = valuesAdditionalPath
|
||||
}
|
||||
|
||||
firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in
|
||||
let avAsset = AVURLAsset(url: URL(fileURLWithPath: path))
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in
|
||||
if let cgImage {
|
||||
if let additionalPath {
|
||||
let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath))
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in
|
||||
if let additionalCGImage {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), UIImage(cgImage: additionalCGImage)))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
})
|
||||
return ActionDisposable {
|
||||
avAssetGenerator.cancelAllCGImageGeneration()
|
||||
}
|
||||
}
|
||||
case let .videoCollage(items):
|
||||
var maxDurationItem: (Double, Subject.VideoCollageItem)?
|
||||
for item in items {
|
||||
switch item.content {
|
||||
case .image:
|
||||
break
|
||||
case let .video(_, duration):
|
||||
if let (maxDuration, _) = maxDurationItem {
|
||||
if duration > maxDuration {
|
||||
maxDurationItem = (duration, item)
|
||||
}
|
||||
} else {
|
||||
maxDurationItem = (duration, item)
|
||||
}
|
||||
case let .asset(asset):
|
||||
if let (maxDuration, _) = maxDurationItem {
|
||||
if asset.duration > maxDuration {
|
||||
maxDurationItem = (asset.duration, item)
|
||||
}
|
||||
} else {
|
||||
maxDurationItem = (asset.duration, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
guard let (maxDuration, mainItem) = maxDurationItem else {
|
||||
fatalError()
|
||||
}
|
||||
switch mainItem.content {
|
||||
case let .video(path, _):
|
||||
videoResult = .single(.videoFile(path: path))
|
||||
case let .asset(asset):
|
||||
videoResult = .single(.asset(localIdentifier: asset.localIdentifier))
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
let image = generateImage(storyDimensions, opaque: false, scale: 1.0, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
})!
|
||||
firstFrame = .single((image, nil))
|
||||
if let videoTrimRange = mediaEditor.values.videoTrimRange {
|
||||
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
||||
} else {
|
||||
duration = min(maxDuration, storyMaxVideoDuration)
|
||||
}
|
||||
case let .asset(asset):
|
||||
videoResult = .single(.asset(localIdentifier: asset.localIdentifier))
|
||||
if asset.mediaType == .video {
|
||||
if let videoTrimRange = mediaEditor.values.videoTrimRange {
|
||||
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
||||
} else {
|
||||
duration = min(asset.duration, storyMaxVideoDuration)
|
||||
}
|
||||
} else {
|
||||
duration = 5.0
|
||||
}
|
||||
|
||||
var additionalPath: String?
|
||||
if let valuesAdditionalPath = mediaEditor.values.additionalVideoPath {
|
||||
additionalPath = valuesAdditionalPath
|
||||
}
|
||||
|
||||
firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in
|
||||
if asset.mediaType == .video {
|
||||
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in
|
||||
if let avAsset {
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in
|
||||
if let cgImage {
|
||||
if let additionalPath {
|
||||
let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath))
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in
|
||||
if let additionalCGImage {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), UIImage(cgImage: additionalCGImage)))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let options = PHImageRequestOptions()
|
||||
options.deliveryMode = .highQualityFormat
|
||||
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in
|
||||
if let image {
|
||||
if let additionalPath {
|
||||
let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath))
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in
|
||||
if let additionalCGImage {
|
||||
subscriber.putNext((image, UIImage(cgImage: additionalCGImage)))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putNext((image, nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
subscriber.putNext((image, nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return EmptyDisposable
|
||||
}
|
||||
case let .draft(draft, _):
|
||||
let draftPath = draft.fullPath(engine: context.engine)
|
||||
if draft.isVideo {
|
||||
videoResult = .single(.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: draftPath))
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in
|
||||
if let cgImage {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
})
|
||||
return ActionDisposable {
|
||||
avAssetGenerator.cancelAllCGImageGeneration()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
videoResult = .single(.imageFile(path: draftPath))
|
||||
duration = 5.0
|
||||
|
||||
if let image = UIImage(contentsOfFile: draftPath) {
|
||||
firstFrame = .single((image, nil))
|
||||
} else {
|
||||
firstFrame = .single((UIImage(), nil))
|
||||
}
|
||||
}
|
||||
case .message, .gift:
|
||||
let peerId: EnginePeer.Id
|
||||
if case let .message(messageIds) = subject {
|
||||
peerId = messageIds.first!.peerId
|
||||
} else {
|
||||
peerId = self.context.account.peerId
|
||||
}
|
||||
|
||||
let isNightTheme = mediaEditor.values.nightTheme
|
||||
let wallpaper = getChatWallpaperImage(context: self.context, peerId: peerId)
|
||||
|> map { _, image, nightImage -> UIImage? in
|
||||
if isNightTheme {
|
||||
return nightImage ?? image
|
||||
} else {
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
videoResult = wallpaper
|
||||
|> mapToSignal { image in
|
||||
if let image {
|
||||
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
|
||||
if let data = image.jpegData(compressionQuality: 0.85) {
|
||||
try? data.write(to: URL(fileURLWithPath: tempImagePath))
|
||||
}
|
||||
return .single(.imageFile(path: tempImagePath))
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|
||||
firstFrame = wallpaper
|
||||
|> map { image in
|
||||
return (image, nil)
|
||||
}
|
||||
duration = 5.0
|
||||
case .sticker:
|
||||
let image = generateImage(storyDimensions, contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
}, opaque: false, scale: 1.0)
|
||||
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).png"
|
||||
if let data = image?.pngData() {
|
||||
try? data.write(to: URL(fileURLWithPath: tempImagePath))
|
||||
}
|
||||
videoResult = .single(.imageFile(path: tempImagePath))
|
||||
duration = 3.0
|
||||
|
||||
firstFrame = .single((image, nil))
|
||||
case .assets:
|
||||
fatalError()
|
||||
}
|
||||
|
||||
let _ = combineLatest(queue: Queue.mainQueue(), firstFrame, videoResult)
|
||||
.start(next: { [weak self] images, videoResult in
|
||||
if let self {
|
||||
let (image, additionalImage) = images
|
||||
var currentImage = mediaEditor.resultImage
|
||||
if let image {
|
||||
mediaEditor.replaceSource(image, additionalImage: additionalImage, time: firstFrameTime, mirror: true)
|
||||
if let updatedImage = mediaEditor.getResultImage(mirror: videoIsMirrored) {
|
||||
currentImage = updatedImage
|
||||
}
|
||||
}
|
||||
|
||||
var inputImage: UIImage
|
||||
if let currentImage {
|
||||
inputImage = currentImage
|
||||
} else if let image {
|
||||
inputImage = image
|
||||
} else {
|
||||
inputImage = UIImage()
|
||||
}
|
||||
|
||||
makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: inputImage, dimensions: storyDimensions, values: mediaEditor.values, time: firstFrameTime, textScale: 2.0, completion: { [weak self] coverImage in
|
||||
if let self {
|
||||
self.willComplete(coverImage, true, { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
Logger.shared.log("MediaEditor", "Completed with video \(videoResult)")
|
||||
self.completion([MediaEditorScreenImpl.Result(media: .video(video: videoResult, coverImage: coverImage, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions), mediaAreas: mediaAreas, caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
finished()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if case let .draft(draft, id) = actualSubject, id == nil {
|
||||
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false)
|
||||
}
|
||||
} else if let image = mediaEditor.resultImage {
|
||||
self.saveDraft(id: randomId)
|
||||
|
||||
var values = mediaEditor.values
|
||||
var outputDimensions: CGSize?
|
||||
if case .avatarEditor = self.mode {
|
||||
outputDimensions = CGSize(width: 640.0, height: 640.0)
|
||||
values = values.withUpdatedQualityPreset(.profile)
|
||||
}
|
||||
makeEditorImageComposition(
|
||||
context: self.node.ciContext,
|
||||
postbox: self.context.account.postbox,
|
||||
inputImage: image,
|
||||
dimensions: storyDimensions,
|
||||
outputDimensions: outputDimensions,
|
||||
values: values,
|
||||
time: .zero,
|
||||
textScale: 2.0,
|
||||
completion: { [weak self] resultImage in
|
||||
if let self, let resultImage {
|
||||
self.willComplete(resultImage, false, { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
Logger.shared.log("MediaEditor", "Completed with image \(resultImage)")
|
||||
self.completion([MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), mediaAreas: mediaAreas, caption: caption, coverTimestamp: nil, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
finished()
|
||||
}
|
||||
})
|
||||
})
|
||||
if case let .draft(draft, id) = actualSubject, id == nil {
|
||||
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func updateMediaEditorEntities() {
|
||||
guard let mediaEditor = self.node.mediaEditor else {
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
private var didComplete = false
|
||||
func requestStoryCompletion(animated: Bool) {
|
||||
guard let mediaEditor = self.node.mediaEditor, !self.didComplete else {
|
||||
return
|
||||
}
|
||||
|
||||
self.didComplete = true
|
||||
|
||||
self.updateMediaEditorEntities()
|
||||
|
||||
mediaEditor.stop()
|
||||
mediaEditor.invalidate()
|
||||
self.node.entitiesView.invalidate()
|
||||
|
||||
if let navigationController = self.navigationController as? NavigationController {
|
||||
navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate)
|
||||
}
|
||||
|
||||
var multipleItems: [EditingItem] = []
|
||||
var isLongVideo = false
|
||||
if self.node.items.count > 1 {
|
||||
multipleItems = self.node.items.filter({ $0.isEnabled })
|
||||
} else if case let .asset(asset) = self.node.subject {
|
||||
let duration: Double
|
||||
if let playerDuration = mediaEditor.duration {
|
||||
duration = playerDuration
|
||||
} else {
|
||||
duration = asset.duration
|
||||
}
|
||||
if duration > storyMaxVideoDuration {
|
||||
let originalDuration = mediaEditor.originalDuration ?? asset.duration
|
||||
let values = mediaEditor.values
|
||||
|
||||
let storyCount = min(storyMaxCombinedVideoCount, Int(ceil(duration / storyMaxVideoDuration)))
|
||||
var start = values.videoTrimRange?.lowerBound ?? 0
|
||||
for i in 0 ..< storyCount {
|
||||
let trimmedValues = values.withUpdatedVideoTrimRange(start ..< min(start + storyMaxVideoDuration, originalDuration))
|
||||
|
||||
var editingItem = EditingItem(asset: asset)
|
||||
if i == 0 {
|
||||
editingItem.caption = self.node.getCaption()
|
||||
}
|
||||
editingItem.values = trimmedValues
|
||||
multipleItems.append(editingItem)
|
||||
|
||||
start += storyMaxVideoDuration
|
||||
}
|
||||
isLongVideo = true
|
||||
}
|
||||
}
|
||||
|
||||
if multipleItems.count > 1 {
|
||||
self.processMultipleItems(items: multipleItems, isLongVideo: isLongVideo)
|
||||
} else {
|
||||
self.processSingleItem()
|
||||
}
|
||||
|
||||
self.dismissAllTooltips()
|
||||
}
|
||||
|
||||
func requestStickerCompletion(animated: Bool) {
|
||||
guard let mediaEditor = self.node.mediaEditor else {
|
||||
return
|
||||
@ -8257,13 +7448,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
let values = mediaEditor.values.withUpdatedCoverDimensions(dimensions)
|
||||
makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, outputDimensions: dimensions.aspectFitted(CGSize(width: 1080, height: 1080)), values: values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in
|
||||
if let self, let resultImage {
|
||||
#if DEBUG
|
||||
if let data = resultImage.jpegData(compressionQuality: 0.7) {
|
||||
let path = NSTemporaryDirectory() + "\(Int(Date().timeIntervalSince1970)).jpg"
|
||||
try? data.write(to: URL(fileURLWithPath: path))
|
||||
}
|
||||
#endif
|
||||
|
||||
self.completion([MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)))], { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
@ -9105,7 +8289,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
self.node.updateEditProgress(progress, cancel: cancel)
|
||||
}
|
||||
|
||||
fileprivate func dismissAllTooltips() {
|
||||
func dismissAllTooltips() {
|
||||
self.window?.forEachController({ controller in
|
||||
if let controller = controller as? TooltipScreen {
|
||||
controller.dismiss()
|
||||
|
@ -0,0 +1,837 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AVFoundation
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TextFormat
|
||||
import Photos
|
||||
import MediaEditor
|
||||
import DrawingUI
|
||||
|
||||
extension MediaEditorScreenImpl {
|
||||
func requestStoryCompletion(animated: Bool) {
|
||||
guard let mediaEditor = self.node.mediaEditor, !self.didComplete else {
|
||||
return
|
||||
}
|
||||
|
||||
self.didComplete = true
|
||||
|
||||
self.updateMediaEditorEntities()
|
||||
|
||||
mediaEditor.stop()
|
||||
mediaEditor.invalidate()
|
||||
self.node.entitiesView.invalidate()
|
||||
|
||||
if let navigationController = self.navigationController as? NavigationController {
|
||||
navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate)
|
||||
}
|
||||
|
||||
var multipleItems: [EditingItem] = []
|
||||
var isLongVideo = false
|
||||
if self.node.items.count > 1 {
|
||||
multipleItems = self.node.items.filter({ $0.isEnabled })
|
||||
} else if case let .asset(asset) = self.node.subject {
|
||||
let duration: Double
|
||||
if let playerDuration = mediaEditor.duration {
|
||||
duration = playerDuration
|
||||
} else {
|
||||
duration = asset.duration
|
||||
}
|
||||
if duration > storyMaxVideoDuration {
|
||||
let originalDuration = mediaEditor.originalDuration ?? asset.duration
|
||||
let values = mediaEditor.values
|
||||
|
||||
let storyCount = min(storyMaxCombinedVideoCount, Int(ceil(duration / storyMaxVideoDuration)))
|
||||
var start = values.videoTrimRange?.lowerBound ?? 0
|
||||
for i in 0 ..< storyCount {
|
||||
let trimmedValues = values.withUpdatedVideoTrimRange(start ..< min(start + storyMaxVideoDuration, originalDuration))
|
||||
|
||||
var editingItem = EditingItem(asset: asset)
|
||||
if i == 0 {
|
||||
editingItem.caption = self.node.getCaption()
|
||||
}
|
||||
editingItem.values = trimmedValues
|
||||
multipleItems.append(editingItem)
|
||||
|
||||
start += storyMaxVideoDuration
|
||||
}
|
||||
isLongVideo = true
|
||||
}
|
||||
}
|
||||
|
||||
if multipleItems.count > 1 {
|
||||
self.processMultipleItems(items: multipleItems, isLongVideo: isLongVideo)
|
||||
} else {
|
||||
self.processSingleItem()
|
||||
}
|
||||
|
||||
self.dismissAllTooltips()
|
||||
}
|
||||
|
||||
private func processSingleItem() {
|
||||
guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, let actualSubject = self.node.actualSubject else {
|
||||
return
|
||||
}
|
||||
|
||||
var caption = self.node.getCaption()
|
||||
caption = convertMarkdownToAttributes(caption)
|
||||
|
||||
var hasEntityChanges = false
|
||||
let randomId: Int64
|
||||
if case let .draft(_, id) = actualSubject, let id {
|
||||
randomId = id
|
||||
} else {
|
||||
randomId = Int64.random(in: .min ... .max)
|
||||
}
|
||||
|
||||
let codableEntities = mediaEditor.values.entities
|
||||
var mediaAreas: [MediaArea] = []
|
||||
if case let .draft(draft, _) = actualSubject {
|
||||
if draft.values.entities != codableEntities {
|
||||
hasEntityChanges = true
|
||||
}
|
||||
} else {
|
||||
mediaAreas = self.initialMediaAreas ?? []
|
||||
}
|
||||
|
||||
var stickers: [TelegramMediaFile] = []
|
||||
for entity in codableEntities {
|
||||
switch entity {
|
||||
case let .sticker(stickerEntity):
|
||||
if case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
|
||||
stickers.append(file.media)
|
||||
}
|
||||
case let .text(textEntity):
|
||||
if let subEntities = textEntity.renderSubEntities {
|
||||
for entity in subEntities {
|
||||
if let stickerEntity = entity as? DrawingStickerEntity, case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
|
||||
stickers.append(file.media)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
if let mediaArea = entity.mediaArea {
|
||||
mediaAreas.append(mediaArea)
|
||||
}
|
||||
}
|
||||
|
||||
var hasAnyChanges = self.node.hasAnyChanges
|
||||
if self.isEditingStoryCover {
|
||||
hasAnyChanges = false
|
||||
}
|
||||
|
||||
if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) {
|
||||
self.saveDraft(id: randomId, isEdit: true)
|
||||
|
||||
self.completion([MediaEditorScreenImpl.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
finished()
|
||||
}
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !(self.isEditingStory || self.isEditingStoryCover) {
|
||||
let privacy = self.state.privacy
|
||||
let _ = updateMediaEditorStoredStateInteractively(engine: self.context.engine, { current in
|
||||
if let current {
|
||||
return current.withUpdatedPrivacy(privacy)
|
||||
} else {
|
||||
return MediaEditorStoredState(privacy: privacy, textSettings: nil)
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
|
||||
if mediaEditor.resultIsVideo {
|
||||
self.saveDraft(id: randomId)
|
||||
|
||||
var firstFrame: Signal<(UIImage?, UIImage?), NoError>
|
||||
let firstFrameTime: CMTime
|
||||
if let coverImageTimestamp = mediaEditor.values.coverImageTimestamp {
|
||||
firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60))
|
||||
} else {
|
||||
firstFrameTime = CMTime(seconds: mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60))
|
||||
}
|
||||
let videoResult: Signal<MediaResult.VideoResult, NoError>
|
||||
var videoIsMirrored = false
|
||||
let duration: Double
|
||||
switch subject {
|
||||
case let .empty(dimensions):
|
||||
let image = generateImage(dimensions.cgSize, opaque: false, scale: 1.0, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
})!
|
||||
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
|
||||
if let data = image.jpegData(compressionQuality: 0.85) {
|
||||
try? data.write(to: URL(fileURLWithPath: tempImagePath))
|
||||
}
|
||||
videoResult = .single(.imageFile(path: tempImagePath))
|
||||
duration = 3.0
|
||||
|
||||
firstFrame = .single((image, nil))
|
||||
case let .image(image, _, _, _):
|
||||
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
|
||||
if let data = image.jpegData(compressionQuality: 0.85) {
|
||||
try? data.write(to: URL(fileURLWithPath: tempImagePath))
|
||||
}
|
||||
videoResult = .single(.imageFile(path: tempImagePath))
|
||||
duration = 5.0
|
||||
|
||||
firstFrame = .single((image, nil))
|
||||
case let .video(path, _, mirror, additionalPath, _, _, durationValue, _, _):
|
||||
videoIsMirrored = mirror
|
||||
videoResult = .single(.videoFile(path: path))
|
||||
if let videoTrimRange = mediaEditor.values.videoTrimRange {
|
||||
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
||||
} else {
|
||||
duration = durationValue
|
||||
}
|
||||
|
||||
var additionalPath = additionalPath
|
||||
if additionalPath == nil, let valuesAdditionalPath = mediaEditor.values.additionalVideoPath {
|
||||
additionalPath = valuesAdditionalPath
|
||||
}
|
||||
|
||||
firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in
|
||||
let avAsset = AVURLAsset(url: URL(fileURLWithPath: path))
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in
|
||||
if let cgImage {
|
||||
if let additionalPath {
|
||||
let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath))
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in
|
||||
if let additionalCGImage {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), UIImage(cgImage: additionalCGImage)))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
})
|
||||
return ActionDisposable {
|
||||
avAssetGenerator.cancelAllCGImageGeneration()
|
||||
}
|
||||
}
|
||||
case let .videoCollage(items):
|
||||
var maxDurationItem: (Double, Subject.VideoCollageItem)?
|
||||
for item in items {
|
||||
switch item.content {
|
||||
case .image:
|
||||
break
|
||||
case let .video(_, duration):
|
||||
if let (maxDuration, _) = maxDurationItem {
|
||||
if duration > maxDuration {
|
||||
maxDurationItem = (duration, item)
|
||||
}
|
||||
} else {
|
||||
maxDurationItem = (duration, item)
|
||||
}
|
||||
case let .asset(asset):
|
||||
if let (maxDuration, _) = maxDurationItem {
|
||||
if asset.duration > maxDuration {
|
||||
maxDurationItem = (asset.duration, item)
|
||||
}
|
||||
} else {
|
||||
maxDurationItem = (asset.duration, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
guard let (maxDuration, mainItem) = maxDurationItem else {
|
||||
fatalError()
|
||||
}
|
||||
switch mainItem.content {
|
||||
case let .video(path, _):
|
||||
videoResult = .single(.videoFile(path: path))
|
||||
case let .asset(asset):
|
||||
videoResult = .single(.asset(localIdentifier: asset.localIdentifier))
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
let image = generateImage(storyDimensions, opaque: false, scale: 1.0, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
})!
|
||||
firstFrame = .single((image, nil))
|
||||
if let videoTrimRange = mediaEditor.values.videoTrimRange {
|
||||
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
||||
} else {
|
||||
duration = min(maxDuration, storyMaxVideoDuration)
|
||||
}
|
||||
case let .asset(asset):
|
||||
videoResult = .single(.asset(localIdentifier: asset.localIdentifier))
|
||||
if asset.mediaType == .video {
|
||||
if let videoTrimRange = mediaEditor.values.videoTrimRange {
|
||||
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
||||
} else {
|
||||
duration = min(asset.duration, storyMaxVideoDuration)
|
||||
}
|
||||
} else {
|
||||
duration = 5.0
|
||||
}
|
||||
|
||||
var additionalPath: String?
|
||||
if let valuesAdditionalPath = mediaEditor.values.additionalVideoPath {
|
||||
additionalPath = valuesAdditionalPath
|
||||
}
|
||||
|
||||
firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in
|
||||
if asset.mediaType == .video {
|
||||
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in
|
||||
if let avAsset {
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in
|
||||
if let cgImage {
|
||||
if let additionalPath {
|
||||
let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath))
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in
|
||||
if let additionalCGImage {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), UIImage(cgImage: additionalCGImage)))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let options = PHImageRequestOptions()
|
||||
options.deliveryMode = .highQualityFormat
|
||||
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in
|
||||
if let image {
|
||||
if let additionalPath {
|
||||
let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath))
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in
|
||||
if let additionalCGImage {
|
||||
subscriber.putNext((image, UIImage(cgImage: additionalCGImage)))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putNext((image, nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
subscriber.putNext((image, nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return EmptyDisposable
|
||||
}
|
||||
case let .draft(draft, _):
|
||||
let draftPath = draft.fullPath(engine: context.engine)
|
||||
if draft.isVideo {
|
||||
videoResult = .single(.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: draftPath))
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in
|
||||
if let cgImage {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
})
|
||||
return ActionDisposable {
|
||||
avAssetGenerator.cancelAllCGImageGeneration()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
videoResult = .single(.imageFile(path: draftPath))
|
||||
duration = 5.0
|
||||
|
||||
if let image = UIImage(contentsOfFile: draftPath) {
|
||||
firstFrame = .single((image, nil))
|
||||
} else {
|
||||
firstFrame = .single((UIImage(), nil))
|
||||
}
|
||||
}
|
||||
case .message, .gift:
|
||||
let peerId: EnginePeer.Id
|
||||
if case let .message(messageIds) = subject {
|
||||
peerId = messageIds.first!.peerId
|
||||
} else {
|
||||
peerId = self.context.account.peerId
|
||||
}
|
||||
|
||||
let isNightTheme = mediaEditor.values.nightTheme
|
||||
let wallpaper = getChatWallpaperImage(context: self.context, peerId: peerId)
|
||||
|> map { _, image, nightImage -> UIImage? in
|
||||
if isNightTheme {
|
||||
return nightImage ?? image
|
||||
} else {
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
videoResult = wallpaper
|
||||
|> mapToSignal { image in
|
||||
if let image {
|
||||
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
|
||||
if let data = image.jpegData(compressionQuality: 0.85) {
|
||||
try? data.write(to: URL(fileURLWithPath: tempImagePath))
|
||||
}
|
||||
return .single(.imageFile(path: tempImagePath))
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|
||||
firstFrame = wallpaper
|
||||
|> map { image in
|
||||
return (image, nil)
|
||||
}
|
||||
duration = 5.0
|
||||
case .sticker:
|
||||
let image = generateImage(storyDimensions, contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
}, opaque: false, scale: 1.0)
|
||||
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).png"
|
||||
if let data = image?.pngData() {
|
||||
try? data.write(to: URL(fileURLWithPath: tempImagePath))
|
||||
}
|
||||
videoResult = .single(.imageFile(path: tempImagePath))
|
||||
duration = 3.0
|
||||
|
||||
firstFrame = .single((image, nil))
|
||||
case .assets:
|
||||
fatalError()
|
||||
}
|
||||
|
||||
let _ = combineLatest(queue: Queue.mainQueue(), firstFrame, videoResult)
|
||||
.start(next: { [weak self] images, videoResult in
|
||||
if let self {
|
||||
let (image, additionalImage) = images
|
||||
var currentImage = mediaEditor.resultImage
|
||||
if let image {
|
||||
mediaEditor.replaceSource(image, additionalImage: additionalImage, time: firstFrameTime, mirror: true)
|
||||
if let updatedImage = mediaEditor.getResultImage(mirror: videoIsMirrored) {
|
||||
currentImage = updatedImage
|
||||
}
|
||||
}
|
||||
|
||||
var inputImage: UIImage
|
||||
if let currentImage {
|
||||
inputImage = currentImage
|
||||
} else if let image {
|
||||
inputImage = image
|
||||
} else {
|
||||
inputImage = UIImage()
|
||||
}
|
||||
|
||||
var values = mediaEditor.values
|
||||
if case .avatarEditor = self.mode, values.videoTrimRange == nil && duration > avatarMaxVideoDuration {
|
||||
values = values.withUpdatedVideoTrimRange(0 ..< avatarMaxVideoDuration)
|
||||
}
|
||||
|
||||
makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: inputImage, dimensions: storyDimensions, values: values, time: firstFrameTime, textScale: 2.0, completion: { [weak self] coverImage in
|
||||
if let self {
|
||||
self.willComplete(coverImage, true, { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
Logger.shared.log("MediaEditor", "Completed with video \(videoResult)")
|
||||
self.completion([MediaEditorScreenImpl.Result(media: .video(video: videoResult, coverImage: coverImage, values: values, duration: duration, dimensions: values.resultDimensions), mediaAreas: mediaAreas, caption: caption, coverTimestamp: values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
finished()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if case let .draft(draft, id) = actualSubject, id == nil {
|
||||
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false)
|
||||
}
|
||||
} else if let image = mediaEditor.resultImage {
|
||||
self.saveDraft(id: randomId)
|
||||
|
||||
var values = mediaEditor.values
|
||||
var outputDimensions: CGSize?
|
||||
if case .avatarEditor = self.mode {
|
||||
outputDimensions = CGSize(width: 640.0, height: 640.0)
|
||||
values = values.withUpdatedQualityPreset(.profile)
|
||||
}
|
||||
makeEditorImageComposition(
|
||||
context: self.node.ciContext,
|
||||
postbox: self.context.account.postbox,
|
||||
inputImage: image,
|
||||
dimensions: storyDimensions,
|
||||
outputDimensions: outputDimensions,
|
||||
values: values,
|
||||
time: .zero,
|
||||
textScale: 2.0,
|
||||
completion: { [weak self] resultImage in
|
||||
if let self, let resultImage {
|
||||
self.willComplete(resultImage, false, { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
Logger.shared.log("MediaEditor", "Completed with image \(resultImage)")
|
||||
self.completion([MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), mediaAreas: mediaAreas, caption: caption, coverTimestamp: nil, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
finished()
|
||||
}
|
||||
})
|
||||
})
|
||||
if case let .draft(draft, id) = actualSubject, id == nil {
|
||||
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func processMultipleItems(items: [EditingItem], isLongVideo: Bool) {
|
||||
guard !items.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
var items = items
|
||||
if let mediaEditor = self.node.mediaEditor, case let .asset(asset) = self.node.subject, let currentItemIndex = items.firstIndex(where: { $0.asset.localIdentifier == asset.localIdentifier }) {
|
||||
var updatedCurrentItem = items[currentItemIndex]
|
||||
updatedCurrentItem.caption = self.node.getCaption()
|
||||
updatedCurrentItem.values = mediaEditor.values
|
||||
items[currentItemIndex] = updatedCurrentItem
|
||||
}
|
||||
|
||||
let multipleResults = Atomic<[MediaEditorScreenImpl.Result]>(value: [])
|
||||
let totalItems = items.count
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
let privacy = self.state.privacy
|
||||
|
||||
if !(self.isEditingStory || self.isEditingStoryCover) {
|
||||
let _ = updateMediaEditorStoredStateInteractively(engine: self.context.engine, { current in
|
||||
if let current {
|
||||
return current.withUpdatedPrivacy(privacy)
|
||||
} else {
|
||||
return MediaEditorStoredState(privacy: privacy, textSettings: nil)
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
|
||||
var order: [Int64] = []
|
||||
for (index, item) in items.enumerated() {
|
||||
guard item.isEnabled else {
|
||||
continue
|
||||
}
|
||||
|
||||
dispatchGroup.enter()
|
||||
|
||||
let randomId = Int64.random(in: .min ... .max)
|
||||
order.append(randomId)
|
||||
|
||||
if item.asset.mediaType == .video {
|
||||
processVideoItem(item: item, index: index, randomId: randomId, isLongVideo: isLongVideo) { result in
|
||||
let _ = multipleResults.modify { results in
|
||||
var updatedResults = results
|
||||
updatedResults.append(result)
|
||||
return updatedResults
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
} else if item.asset.mediaType == .image {
|
||||
processImageItem(item: item, index: index, randomId: randomId) { result in
|
||||
let _ = multipleResults.modify { results in
|
||||
var updatedResults = results
|
||||
updatedResults.append(result)
|
||||
return updatedResults
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
} else {
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
let results = multipleResults.with { $0 }
|
||||
if results.count == totalItems {
|
||||
var orderedResults: [MediaEditorScreenImpl.Result] = []
|
||||
for id in order {
|
||||
if let item = results.first(where: { $0.randomId == id }) {
|
||||
orderedResults.append(item)
|
||||
}
|
||||
}
|
||||
self.completion(results, { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
finished()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func processVideoItem(item: EditingItem, index: Int, randomId: Int64, isLongVideo: Bool, completion: @escaping (MediaEditorScreenImpl.Result) -> Void) {
|
||||
let asset = item.asset
|
||||
|
||||
let itemMediaEditor = setupMediaEditorForItem(item: item)
|
||||
|
||||
var caption = item.caption
|
||||
caption = convertMarkdownToAttributes(caption)
|
||||
|
||||
var mediaAreas: [MediaArea] = []
|
||||
var stickers: [TelegramMediaFile] = []
|
||||
|
||||
if let entities = item.values?.entities {
|
||||
for entity in entities {
|
||||
if let mediaArea = entity.mediaArea {
|
||||
mediaAreas.append(mediaArea)
|
||||
}
|
||||
extractStickersFromEntity(entity, into: &stickers)
|
||||
}
|
||||
}
|
||||
|
||||
let firstFrameTime: CMTime
|
||||
if let coverImageTimestamp = item.values?.coverImageTimestamp, !isLongVideo || index == 0 {
|
||||
firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60))
|
||||
} else {
|
||||
firstFrameTime = CMTime(seconds: item.values?.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60))
|
||||
}
|
||||
|
||||
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { [weak self] avAsset, _, _ in
|
||||
guard let avAsset else {
|
||||
DispatchQueue.main.async {
|
||||
if let self {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let duration: Double
|
||||
if let videoTrimRange = item.values?.videoTrimRange {
|
||||
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
||||
} else {
|
||||
duration = min(asset.duration, storyMaxVideoDuration)
|
||||
}
|
||||
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)]) { [weak self] _, cgImage, _, _, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
if let cgImage {
|
||||
let image = UIImage(cgImage: cgImage)
|
||||
itemMediaEditor.replaceSource(image, additionalImage: nil, time: firstFrameTime, mirror: false)
|
||||
|
||||
if let resultImage = itemMediaEditor.resultImage {
|
||||
makeEditorImageComposition(
|
||||
context: self.node.ciContext,
|
||||
postbox: self.context.account.postbox,
|
||||
inputImage: resultImage,
|
||||
dimensions: storyDimensions,
|
||||
values: itemMediaEditor.values,
|
||||
time: firstFrameTime,
|
||||
textScale: 2.0
|
||||
) { coverImage in
|
||||
if let coverImage = coverImage {
|
||||
let result = MediaEditorScreenImpl.Result(
|
||||
media: .video(
|
||||
video: .asset(localIdentifier: asset.localIdentifier),
|
||||
coverImage: coverImage,
|
||||
values: itemMediaEditor.values,
|
||||
duration: duration,
|
||||
dimensions: itemMediaEditor.values.resultDimensions
|
||||
),
|
||||
mediaAreas: mediaAreas,
|
||||
caption: caption,
|
||||
coverTimestamp: itemMediaEditor.values.coverImageTimestamp,
|
||||
options: self.state.privacy,
|
||||
stickers: stickers,
|
||||
randomId: randomId
|
||||
)
|
||||
completion(result)
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func processImageItem(item: EditingItem, index: Int, randomId: Int64, completion: @escaping (MediaEditorScreenImpl.Result) -> Void) {
|
||||
let asset = item.asset
|
||||
|
||||
let itemMediaEditor = setupMediaEditorForItem(item: item)
|
||||
|
||||
var caption = item.caption
|
||||
caption = convertMarkdownToAttributes(caption)
|
||||
|
||||
var mediaAreas: [MediaArea] = []
|
||||
var stickers: [TelegramMediaFile] = []
|
||||
|
||||
if let entities = item.values?.entities {
|
||||
for entity in entities {
|
||||
if let mediaArea = entity.mediaArea {
|
||||
mediaAreas.append(mediaArea)
|
||||
}
|
||||
extractStickersFromEntity(entity, into: &stickers)
|
||||
}
|
||||
}
|
||||
|
||||
let options = PHImageRequestOptions()
|
||||
options.deliveryMode = .highQualityFormat
|
||||
options.isNetworkAccessAllowed = true
|
||||
|
||||
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { [weak self] image, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
if let image {
|
||||
itemMediaEditor.replaceSource(image, additionalImage: nil, time: .zero, mirror: false)
|
||||
|
||||
if let resultImage = itemMediaEditor.resultImage {
|
||||
makeEditorImageComposition(
|
||||
context: self.node.ciContext,
|
||||
postbox: self.context.account.postbox,
|
||||
inputImage: resultImage,
|
||||
dimensions: storyDimensions,
|
||||
values: itemMediaEditor.values,
|
||||
time: .zero,
|
||||
textScale: 2.0
|
||||
) { resultImage in
|
||||
if let resultImage = resultImage {
|
||||
let result = MediaEditorScreenImpl.Result(
|
||||
media: .image(
|
||||
image: resultImage,
|
||||
dimensions: PixelDimensions(resultImage.size)
|
||||
),
|
||||
mediaAreas: mediaAreas,
|
||||
caption: caption,
|
||||
coverTimestamp: nil,
|
||||
options: self.state.privacy,
|
||||
stickers: stickers,
|
||||
randomId: randomId
|
||||
)
|
||||
completion(result)
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupMediaEditorForItem(item: EditingItem) -> MediaEditor {
|
||||
var values = item.values
|
||||
if values?.videoTrimRange == nil {
|
||||
values = values?.withUpdatedVideoTrimRange(0 ..< storyMaxVideoDuration)
|
||||
}
|
||||
return MediaEditor(
|
||||
context: self.context,
|
||||
mode: .default,
|
||||
subject: .asset(item.asset),
|
||||
values: values,
|
||||
hasHistogram: false,
|
||||
isStandalone: true
|
||||
)
|
||||
}
|
||||
|
||||
private func extractStickersFromEntity(_ entity: CodableDrawingEntity, into stickers: inout [TelegramMediaFile]) {
|
||||
switch entity {
|
||||
case let .sticker(stickerEntity):
|
||||
if case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
|
||||
stickers.append(file.media)
|
||||
}
|
||||
case let .text(textEntity):
|
||||
if let subEntities = textEntity.renderSubEntities {
|
||||
for entity in subEntities {
|
||||
if let stickerEntity = entity as? DrawingStickerEntity, case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
|
||||
stickers.append(file.media)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func createEmptyResult(randomId: Int64) -> MediaEditorScreenImpl.Result {
|
||||
let emptyImage = UIImage()
|
||||
return MediaEditorScreenImpl.Result(
|
||||
media: .image(
|
||||
image: emptyImage,
|
||||
dimensions: PixelDimensions(emptyImage.size)
|
||||
),
|
||||
mediaAreas: [],
|
||||
caption: NSAttributedString(),
|
||||
coverTimestamp: nil,
|
||||
options: self.state.privacy,
|
||||
stickers: [],
|
||||
randomId: randomId
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
func updateMediaEditorEntities() {
|
||||
guard let mediaEditor = self.node.mediaEditor else {
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
@ -1099,7 +1099,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
||||
}
|
||||
}
|
||||
if let starRefPeerId = transaction.starrefPeerId, let starRefPeer = state.peerMap[starRefPeerId] {
|
||||
if !transaction.flags.contains(.isPaidMessage) {
|
||||
if !transaction.flags.contains(.isPaidMessage) && !transaction.flags.contains(.isStarGiftResale) {
|
||||
tableItems.append(.init(
|
||||
id: "to",
|
||||
title: strings.StarsTransaction_StarRefReason_Affiliate,
|
||||
@ -1130,7 +1130,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
||||
))
|
||||
}
|
||||
|
||||
if let toPeer {
|
||||
if let toPeer, !transaction.flags.contains(.isStarGiftResale) {
|
||||
tableItems.append(.init(
|
||||
id: "referred",
|
||||
title: transaction.flags.contains(.isPaidMessage) ? strings.Stars_Transaction_From : strings.StarsTransaction_StarRefReason_Referred,
|
||||
@ -1162,7 +1162,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
||||
}
|
||||
}
|
||||
if let starrefCommissionPermille = transaction.starrefCommissionPermille, transaction.starrefPeerId != nil {
|
||||
if transaction.flags.contains(.isPaidMessage) {
|
||||
if transaction.flags.contains(.isPaidMessage) || transaction.flags.contains(.isStarGiftResale) {
|
||||
var totalStars = transaction.count
|
||||
if let starrefCount = transaction.starrefAmount {
|
||||
totalStars = totalStars + starrefCount
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "price (2).pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/price (2).pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/price (2).pdf
vendored
Normal file
Binary file not shown.
@ -1946,12 +1946,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
}
|
||||
|
||||
var audioTranscriptionProvidedByBoost = false
|
||||
var autoTranslate = false
|
||||
var isCopyProtectionEnabled: Bool = data.initialData?.peer?.isCopyProtectionEnabled ?? false
|
||||
for entry in view.additionalData {
|
||||
if case let .peer(_, maybePeer) = entry, let peer = maybePeer {
|
||||
isCopyProtectionEnabled = peer.isCopyProtectionEnabled
|
||||
if let channel = peer as? TelegramChannel, let boostLevel = channel.approximateBoostLevel {
|
||||
if boostLevel >= premiumConfiguration.minGroupAudioTranscriptionLevel {
|
||||
if let channel = peer as? TelegramChannel {
|
||||
autoTranslate = channel.flags.contains(.autoTranslateEnabled)
|
||||
if let boostLevel = channel.approximateBoostLevel, boostLevel >= premiumConfiguration.minGroupAudioTranscriptionLevel {
|
||||
audioTranscriptionProvidedByBoost = true
|
||||
}
|
||||
}
|
||||
@ -1964,7 +1966,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
)
|
||||
|
||||
var translateToLanguage: (fromLang: String, toLang: String)?
|
||||
if let translationState, isPremium && translationState.isEnabled {
|
||||
if let translationState, (isPremium || autoTranslate) && translationState.isEnabled {
|
||||
var languageCode = translationState.toLang ?? chatPresentationData.strings.baseLanguageCode
|
||||
let rawSuffix = "-raw"
|
||||
if languageCode.hasSuffix(rawSuffix) {
|
||||
|
@ -187,9 +187,16 @@ final class ChatTranslationPanelNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
let isPremium = self.chatInterfaceState?.isPremium ?? false
|
||||
if isPremium {
|
||||
|
||||
var translationAvailable = isPremium
|
||||
if let channel = self.chatInterfaceState?.renderedPeer?.chatMainPeer as? TelegramChannel, channel.flags.contains(.autoTranslateEnabled) {
|
||||
translationAvailable = true
|
||||
}
|
||||
|
||||
if translationAvailable {
|
||||
self.interfaceInteraction?.toggleTranslation(translationState.isEnabled ? .original : .translated)
|
||||
} else if !translationState.isEnabled {
|
||||
if !isPremium {
|
||||
let context = self.context
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = PremiumDemoScreen(context: context, subject: .translation, action: {
|
||||
@ -202,6 +209,7 @@ final class ChatTranslationPanelNode: ASDisplayNode {
|
||||
self.interfaceInteraction?.chatController()?.push(controller)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func morePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) {
|
||||
guard let translationState = self.chatInterfaceState?.translationState else {
|
||||
|
@ -3551,7 +3551,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}
|
||||
let editorController = MediaEditorScreenImpl(
|
||||
context: context,
|
||||
mode: .storyEditor,
|
||||
mode: .storyEditor(remainingCount: 1),
|
||||
subject: subject,
|
||||
customTarget: nil,
|
||||
initialCaption: text.flatMap { NSAttributedString(string: $0) },
|
||||
@ -3716,7 +3716,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let controller = MediaEditorScreenImpl(
|
||||
context: context,
|
||||
mode: .storyEditor,
|
||||
mode: .storyEditor(remainingCount: 1),
|
||||
subject: editorSubject,
|
||||
transitionIn: nil,
|
||||
transitionOut: { _, _ in
|
||||
|
@ -346,7 +346,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
return nil
|
||||
}
|
||||
},
|
||||
completion: { result, resultTransition, dismissed in
|
||||
completion: { result, resultTransition, storyRemainingCount, dismissed in
|
||||
let subject: Signal<MediaEditorScreenImpl.Subject?, NoError> = result
|
||||
|> map { value -> MediaEditorScreenImpl.Subject? in
|
||||
func editorPIPPosition(_ position: CameraScreenImpl.PIPPosition) -> MediaEditorScreenImpl.PIPPosition {
|
||||
@ -422,7 +422,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
|
||||
let controller = MediaEditorScreenImpl(
|
||||
context: context,
|
||||
mode: .storyEditor,
|
||||
mode: .storyEditor(remainingCount: storyRemainingCount ?? 1),
|
||||
subject: subject,
|
||||
customTarget: mediaEditorCustomTarget,
|
||||
transitionIn: transitionIn,
|
||||
|
Loading…
x
Reference in New Issue
Block a user