Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,606 @@
import Foundation
import UIKit
import LegacyComponents
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import DeviceAccess
import AccountContext
import LegacyUI
import SaveToCameraRoll
import Photos
public func defaultVideoPresetForContext(_ context: AccountContext) -> TGMediaVideoConversionPreset {
var networkType: NetworkType = .wifi
let _ = (context.account.networkType
|> take(1)).start(next: { value in
networkType = value
})
let autodownloadSettings = context.sharedContext.currentAutodownloadSettings.with { $0 }
let presetSettings: AutodownloadPresetSettings
switch networkType {
case .wifi:
presetSettings = autodownloadSettings.highPreset
default:
presetSettings = autodownloadSettings.mediumPreset
}
let effectiveValue: Int
if presetSettings.videoUploadMaxbitrate == 0 {
effectiveValue = 0
} else {
effectiveValue = Int(presetSettings.videoUploadMaxbitrate) * 5 / 100
}
switch effectiveValue {
case 0:
return TGMediaVideoConversionPresetCompressedMedium
case 1:
return TGMediaVideoConversionPresetCompressedVeryLow
case 2:
return TGMediaVideoConversionPresetCompressedLow
case 3:
return TGMediaVideoConversionPresetCompressedMedium
case 4:
return TGMediaVideoConversionPresetCompressedHigh
case 5:
return TGMediaVideoConversionPresetCompressedVeryHigh
default:
return TGMediaVideoConversionPresetCompressedMedium
}
}
public enum LegacyAttachmentMenuMediaEditing {
case none
case imageOrVideo(AnyMediaReference?)
case file
}
public enum LegacyMediaEditorMode {
case draw
case adjustments
}
public func legacyWallpaperEditor(context: AccountContext, item: TGMediaEditableItem, cropRect: CGRect, adjustments: TGMediaEditAdjustments?, referenceView: UIView, beginTransitionOut: ((Bool) -> Void)?, finishTransitionOut: (() -> Void)?, completion: @escaping (UIImage?, TGMediaEditAdjustments?) -> Void, fullSizeCompletion: @escaping (UIImage?) -> Void, present: @escaping (ViewController, Any?) -> Void) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil)
legacyController.blocksBackgroundWhenInOverlay = true
legacyController.acceptsFocusWhenInOverlay = true
legacyController.statusBar.statusBarStyle = .Ignore
legacyController.controllerLoaded = { [weak legacyController] in
legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true
}
let emptyController = LegacyEmptyController(context: legacyController.context)!
emptyController.navigationBarShouldBeHidden = true
let navigationController = makeLegacyNavigationController(rootController: emptyController)
navigationController.setNavigationBarHidden(true, animated: false)
legacyController.bind(controller: navigationController)
legacyController.enableSizeClassSignal = true
present(legacyController, nil)
TGPhotoVideoEditor.present(with: legacyController.context, controller: emptyController, with: item, cropRect: cropRect, adjustments: adjustments, referenceView: referenceView, completion: { image, adjustments in
completion(image, adjustments)
}, fullSizeCompletion: { image in
Queue.mainQueue().async {
fullSizeCompletion(image)
}
}, beginTransitionOut: { saving in
beginTransitionOut?(saving)
}, finishTransitionOut: { [weak legacyController] in
legacyController?.dismiss()
finishTransitionOut?()
})
}
public enum StoryMediaEditorResult {
case image(UIImage)
case video(String)
case asset(PHAsset)
}
public func legacyStoryMediaEditor(context: AccountContext, item: TGMediaEditableItem & TGMediaSelectableItem, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, completion: @escaping (StoryMediaEditorResult) -> Void, present: @escaping (ViewController, Any?) -> Void) {
let paintStickersContext = LegacyPaintStickersContext(context: context)
paintStickersContext.captionPanelView = {
return getCaptionPanelView()
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil)
legacyController.blocksBackgroundWhenInOverlay = true
legacyController.acceptsFocusWhenInOverlay = true
legacyController.statusBar.statusBarStyle = .Ignore
legacyController.controllerLoaded = { [weak legacyController] in
legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true
}
let emptyController = LegacyEmptyController(context: legacyController.context)!
emptyController.navigationBarShouldBeHidden = true
let navigationController = makeLegacyNavigationController(rootController: emptyController)
navigationController.setNavigationBarHidden(true, animated: false)
legacyController.bind(controller: navigationController)
legacyController.enableSizeClassSignal = true
present(legacyController, nil)
TGPhotoVideoEditor.present(with: legacyController.context, controller: emptyController, caption: NSAttributedString(), withItem: item, paint: false, adjustments: false, recipientName: "", stickersContext: paintStickersContext, from: .zero, mainSnapshot: nil, snapshots: [] as [Any], immediate: true, appeared: {
}, completion: { result, editingContext in
var completionResult: Signal<StoryMediaEditorResult, NoError>
if let photo = result as? TGCameraCapturedPhoto {
if let _ = editingContext?.adjustments(for: result) {
completionResult = .single(.image(photo.existingImage))
} else {
completionResult = .single(.image(photo.existingImage))
}
} else if let video = result as? TGCameraCapturedVideo {
completionResult = .single(.video(video.immediateAVAsset.url.absoluteString))
} else if let asset = result as? TGMediaAsset {
completionResult = .single(.asset(asset.backingAsset))
} else {
completionResult = .complete()
}
let _ = (completionResult
|> deliverOnMainQueue).start(next: { value in
completion(value)
})
}, dismissed: { [weak legacyController] in
legacyController?.dismiss()
})
}
public func legacyMediaEditor(context: AccountContext, peer: Peer, threadTitle: String?, media: AnyMediaReference, mode: LegacyMediaEditorMode, initialCaption: NSAttributedString, snapshots: [UIView], transitionCompletion: (() -> Void)?, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) {
let _ = (fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: media)
|> deliverOnMainQueue).start(next: { (value, isImage) in
guard case let .data(data) = value, data.complete else {
return
}
let item: TGMediaEditableItem & TGMediaSelectableItem
if let image = UIImage(contentsOfFile: data.path) {
item = TGCameraCapturedPhoto(existing: image)
} else {
item = TGCameraCapturedVideo(url: URL(fileURLWithPath: data.path))
}
let paintStickersContext = LegacyPaintStickersContext(context: context)
paintStickersContext.captionPanelView = {
return getCaptionPanelView()
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let recipientName: String
if let threadTitle {
recipientName = threadTitle
} else {
if peer.id == context.account.peerId {
recipientName = presentationData.strings.DialogList_SavedMessages
} else {
recipientName = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
}
}
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil)
legacyController.blocksBackgroundWhenInOverlay = true
legacyController.acceptsFocusWhenInOverlay = true
legacyController.statusBar.statusBarStyle = .Ignore
legacyController.controllerLoaded = { [weak legacyController] in
legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true
}
let emptyController = LegacyEmptyController(context: legacyController.context)!
emptyController.navigationBarShouldBeHidden = true
let navigationController = makeLegacyNavigationController(rootController: emptyController)
navigationController.setNavigationBarHidden(true, animated: false)
legacyController.bind(controller: navigationController)
legacyController.enableSizeClassSignal = true
present(legacyController, nil)
TGPhotoVideoEditor.present(with: legacyController.context, controller: emptyController, caption: initialCaption, withItem: item, paint: mode == .draw, adjustments: mode == .adjustments, recipientName: recipientName, stickersContext: paintStickersContext, from: .zero, mainSnapshot: nil, snapshots: snapshots as [Any], immediate: transitionCompletion != nil, appeared: {
transitionCompletion?()
}, completion: { result, editingContext in
let nativeGenerator = legacyAssetPickerItemGenerator()
var selectableResult: TGMediaSelectableItem?
if let result = result {
selectableResult = unsafeDowncast(result, to: TGMediaSelectableItem.self)
}
let signals = TGCameraController.resultSignals(for: nil, editingContext: editingContext, currentItem: selectableResult, storeAssets: false, saveEditedPhotos: false, descriptionGenerator: { _1, _2, _3 in
nativeGenerator(_1, _2, _3, nil)
})
let isCaptionAbove = editingContext?.isCaptionAbove() ?? false
sendMessagesWithSignals(signals, false, 0, isCaptionAbove)
}, dismissed: { [weak legacyController] in
legacyController?.dismiss()
})
})
}
public func legacyAttachmentMenu(
context: AccountContext,
peer: Peer?,
threadTitle: String?,
chatLocation: ChatLocation,
editMediaOptions: LegacyAttachmentMenuMediaEditing?,
addingMedia: Bool,
saveEditedPhotos: Bool,
allowGrouping: Bool,
hasSchedule: Bool,
canSendPolls: Bool,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>),
parentController: LegacyController,
recentlyUsedInlineBots: [Peer],
initialCaption: NSAttributedString,
openGallery: @escaping () -> Void,
openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void,
openFileGallery: @escaping () -> Void,
openWebSearch: @escaping () -> Void,
openMap: @escaping () -> Void,
openContacts: @escaping () -> Void,
openPoll: @escaping () -> Void,
presentSelectionLimitExceeded: @escaping () -> Void,
presentCantSendMultipleFiles: @escaping () -> Void,
presentJpegConversionAlert: @escaping (@escaping (Bool) -> Void) -> Void,
presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void,
presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void,
sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ((String) -> UIView?)?, @escaping () -> Void) -> Void,
selectRecentlyUsedInlineBot: @escaping (Peer) -> Void,
getCaptionPanelView: @escaping () -> TGCaptionPanelView?,
present: @escaping (ViewController, Any?) -> Void
) -> TGMenuSheetController {
let defaultVideoPreset = defaultVideoPresetForContext(context)
UserDefaults.standard.set(defaultVideoPreset.rawValue as NSNumber, forKey: "TG_preferredVideoPreset_v0")
let presentationData = updatedPresentationData.initial
let recipientName: String
if let threadTitle {
recipientName = threadTitle
} else if let peer {
if peer.id == context.account.peerId {
recipientName = presentationData.strings.DialogList_SavedMessages
} else {
recipientName = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
}
} else {
recipientName = ""
}
let actionSheetTheme = ActionSheetControllerTheme(presentationData: presentationData)
let fontSize = floor(actionSheetTheme.baseFontSize * 20.0 / 17.0)
let isSecretChat = peer?.id.namespace == Namespaces.Peer.SecretChat
let controller = TGMenuSheetController(context: parentController.context, dark: false)!
controller.dismissesByOutsideTap = true
controller.hasSwipeGesture = true
controller.maxHeight = 445.0
controller.forceFullScreen = true
var itemViews: [Any] = []
var editing = false
var canSendImageOrVideo = false
var canEditFile = false
var editCurrentMedia: AnyMediaReference?
if let editMediaOptions = editMediaOptions {
switch editMediaOptions {
case .none:
break
case let .imageOrVideo(anyReference):
editCurrentMedia = anyReference
case .file:
canEditFile = true
}
canSendImageOrVideo = true
editing = true
} else {
canSendImageOrVideo = true
}
var carouselItemView: TGAttachmentCarouselItemView?
var underlyingViews: [UIView] = []
var selectionLimit: Int32 = 100
var slowModeEnabled = false
if let channel = peer as? TelegramChannel, channel.isRestrictedBySlowmode {
slowModeEnabled = true
selectionLimit = 10
}
let paintStickersContext = LegacyPaintStickersContext(context: context)
paintStickersContext.captionPanelView = {
return getCaptionPanelView()
}
if canSendImageOrVideo {
let carouselItem = TGAttachmentCarouselItemView(context: parentController.context, camera: PGCamera.cameraAvailable(), selfPortrait: false, forProfilePhoto: false, assetType: TGMediaAssetAnyType, saveEditedPhotos: !isSecretChat && saveEditedPhotos, allowGrouping: editMediaOptions == nil && allowGrouping, allowSelection: editMediaOptions == nil, allowEditing: true, document: false, selectionLimit: selectionLimit)!
carouselItemView = carouselItem
carouselItem.stickersContext = paintStickersContext
carouselItem.recipientName = recipientName
var openedCamera = false
controller.willDismiss = { [weak carouselItem] _ in
if let carouselItem = carouselItem, !openedCamera {
carouselItem.saveStartImage()
}
}
carouselItem.cameraPressed = { [weak controller, weak parentController] cameraView in
openedCamera = true
if let controller = controller {
if let parentController = parentController, parentController.context.currentlyInSplitView() {
return
}
DeviceAccess.authorizeAccess(to: .camera(.video), presentationData: updatedPresentationData.initial, present: context.sharedContext.presentGlobalController, openSettings: context.sharedContext.applicationBindings.openSettings, { value in
if value {
openCamera(cameraView, controller)
}
})
}
}
carouselItem.selectionLimitExceeded = {
presentSelectionLimitExceeded()
}
if let peer, peer.id != context.account.peerId {
if peer is TelegramUser {
carouselItem.hasTimer = hasSchedule
}
carouselItem.hasSilentPosting = true
}
carouselItem.hasSchedule = hasSchedule
carouselItem.reminder = peer?.id == context.account.peerId
carouselItem.presentScheduleController = { media, done in
presentSchedulePicker(media, { time in
done?(time)
})
}
carouselItem.presentTimerController = { done in
presentTimerPicker { time in
done?(time)
}
}
carouselItem.sendPressed = { [weak controller, weak carouselItem] currentItem, asFiles, silentPosting, scheduleTime, isFromPicker in
if let controller = controller, let carouselItem = carouselItem {
let intent: TGMediaAssetsControllerIntent = asFiles ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent
var hasHeic = false
var allItems = carouselItem.selectionContext?.selectedItems() ?? []
if let currentItem = currentItem {
allItems.append(currentItem)
}
for item in allItems {
if item is TGCameraCapturedVideo {
} else if let asset = item as? TGMediaAsset, asset.uniformTypeIdentifier.contains("heic") {
hasHeic = true
break
}
}
if slowModeEnabled, allItems.count > 1 {
presentCantSendMultipleFiles()
} else {
let process: (Bool) -> Void = { convert in
let signals = TGMediaAssetsController.resultSignals(for: carouselItem.selectionContext, editingContext: carouselItem.editingContext, intent: intent, currentItem: currentItem, storeAssets: true, convertToJpeg: convert, descriptionGenerator: legacyAssetPickerItemGenerator(), saveEditedPhotos: saveEditedPhotos)
sendMessagesWithSignals(signals, silentPosting, scheduleTime, isFromPicker ? nil : { [weak carouselItem] uniqueId in
if let carouselItem = carouselItem {
return carouselItem.getItemSnapshot(uniqueId)
}
return nil
}, { [weak controller] in
controller?.dismiss(animated: true)
})
}
if hasHeic && asFiles {
presentJpegConversionAlert({ convert in
process(convert)
})
} else {
process(false)
}
}
}
};
carouselItem.allowCaptions = true
if !initialCaption.string.isEmpty {
carouselItem.editingContext.setForcedCaption(initialCaption)
}
itemViews.append(carouselItem)
let galleryTitle: String
if addingMedia {
galleryTitle = presentationData.strings.AttachmentMenu_AddPhotoOrVideo
} else {
galleryTitle = editing ? presentationData.strings.Conversation_EditingMessageMediaChange : presentationData.strings.AttachmentMenu_PhotoOrVideo
}
let galleryItem = TGMenuSheetButtonItemView(title: galleryTitle, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
controller?.dismiss(animated: true)
openGallery()
})!
if !editing {
galleryItem.longPressAction = { [weak controller] in
if let controller = controller {
controller.dismiss(animated: true)
}
openWebSearch()
}
}
itemViews.append(galleryItem)
underlyingViews.append(galleryItem)
if addingMedia {
let fileItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_AddDocument, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
controller?.dismiss(animated: true)
openFileGallery()
})!
itemViews.append(fileItem)
underlyingViews.append(fileItem)
}
}
if !editing && !addingMedia {
let fileItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
controller?.dismiss(animated: true)
openFileGallery()
})!
itemViews.append(fileItem)
underlyingViews.append(fileItem)
}
if canEditFile && !addingMedia {
let fileItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
controller?.dismiss(animated: true)
openFileGallery()
})!
itemViews.append(fileItem)
}
if let editCurrentMedia = editCurrentMedia {
let title: String
if editCurrentMedia.media is TelegramMediaImage {
title = presentationData.strings.Conversation_EditingMessageMediaEditCurrentPhoto
} else {
title = presentationData.strings.Conversation_EditingMessageMediaEditCurrentVideo
}
let editCurrentItem = TGMenuSheetButtonItemView(title: title, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
controller?.dismiss(animated: true)
let _ = (fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: editCurrentMedia)
|> deliverOnMainQueue).start(next: { (value, isImage) in
guard case let .data(data) = value, data.complete else {
return
}
let item: TGMediaEditableItem & TGMediaSelectableItem
if let image = UIImage(contentsOfFile: data.path) {
item = TGCameraCapturedPhoto(existing: image)
} else {
item = TGCameraCapturedVideo(url: URL(fileURLWithPath: data.path))
}
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil)
legacyController.statusBar.statusBarStyle = .Ignore
legacyController.controllerLoaded = { [weak legacyController] in
legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true
}
let emptyController = LegacyEmptyController(context: legacyController.context)!
emptyController.navigationBarShouldBeHidden = true
let navigationController = makeLegacyNavigationController(rootController: emptyController)
navigationController.setNavigationBarHidden(true, animated: false)
legacyController.bind(controller: navigationController)
let recipientName: String
if let peer {
recipientName = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
} else {
recipientName = ""
}
legacyController.enableSizeClassSignal = true
let presentationDisposable = updatedPresentationData.signal.start(next: { [weak legacyController] presentationData in
if let legacyController = legacyController, let controller = legacyController.legacyController as? TGMenuSheetController {
controller.pallete = legacyMenuPaletteFromTheme(presentationData.theme, forceDark: false)
}
})
legacyController.disposables.add(presentationDisposable)
present(legacyController, nil)
TGPhotoVideoEditor.present(with: legacyController.context, controller: emptyController, caption: initialCaption, withItem: item, paint: false, adjustments: false, recipientName: recipientName, stickersContext: paintStickersContext, from: .zero, mainSnapshot: nil, snapshots: [], immediate: false, appeared: {
}, completion: { result, editingContext in
let nativeGenerator = legacyAssetPickerItemGenerator()
var selectableResult: TGMediaSelectableItem?
if let result = result {
selectableResult = unsafeDowncast(result, to: TGMediaSelectableItem.self)
}
let signals = TGCameraController.resultSignals(for: nil, editingContext: editingContext, currentItem: selectableResult, storeAssets: false, saveEditedPhotos: false, descriptionGenerator: { _1, _2, _3 in
nativeGenerator(_1, _2, _3, nil)
})
sendMessagesWithSignals(signals, false, 0, { _ in nil}, {})
}, dismissed: { [weak legacyController] in
legacyController?.dismiss()
})
})
})!
itemViews.append(editCurrentItem)
}
if editMediaOptions == nil && !addingMedia {
let locationItem = TGMenuSheetButtonItemView(title: presentationData.strings.Conversation_Location, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
controller?.dismiss(animated: true)
openMap()
})!
itemViews.append(locationItem)
var peerSupportsPolls = false
if let peer {
if peer is TelegramGroup || peer is TelegramChannel {
peerSupportsPolls = true
} else if let user = peer as? TelegramUser, let _ = user.botInfo {
peerSupportsPolls = true
}
}
if let peer, peerSupportsPolls, canSendMessagesToPeer(peer) && canSendPolls {
let pollItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_Poll, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
controller?.dismiss(animated: true)
openPoll()
})!
itemViews.append(pollItem)
}
let contactItem = TGMenuSheetButtonItemView(title: presentationData.strings.Conversation_Contact, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
controller?.dismiss(animated: true)
openContacts()
})!
itemViews.append(contactItem)
}
carouselItemView?.underlyingViews = underlyingViews
if editMediaOptions == nil && !addingMedia {
for i in 0 ..< min(20, recentlyUsedInlineBots.count) {
let peer = recentlyUsedInlineBots[i]
let addressName = peer.addressName
if let addressName = addressName {
let botItem = TGMenuSheetButtonItemView(title: "@" + addressName, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
controller?.dismiss(animated: true)
selectRecentlyUsedInlineBot(peer)
})!
botItem.overflow = true
itemViews.append(botItem)
}
}
}
carouselItemView?.remainingHeight = TGMenuSheetButtonItemViewHeight * CGFloat(itemViews.count - 1)
let cancelItem = TGMenuSheetButtonItemView(title: presentationData.strings.Common_Cancel, type: TGMenuSheetButtonTypeCancel, fontSize: actionSheetTheme.baseFontSize, action: { [weak controller] in
controller?.dismiss(animated: true)
})!
itemViews.append(cancelItem)
controller.setItemViews(itemViews)
return controller
}
public func legacyMenuPaletteFromTheme(_ theme: PresentationTheme, forceDark: Bool) -> TGMenuSheetPallete {
let sheetTheme: PresentationThemeActionSheet
if forceDark && !theme.overallDarkAppearance {
sheetTheme = defaultDarkColorPresentationTheme.actionSheet
} else {
sheetTheme = theme.actionSheet
}
return TGMenuSheetPallete(dark: forceDark || theme.overallDarkAppearance, backgroundColor: sheetTheme.opaqueItemBackgroundColor, selectionColor: sheetTheme.opaqueItemHighlightedBackgroundColor, separatorColor: sheetTheme.opaqueItemSeparatorColor, accentColor: sheetTheme.controlAccentColor, destructiveColor: sheetTheme.destructiveActionTextColor, textColor: sheetTheme.primaryTextColor, secondaryTextColor: sheetTheme.secondaryTextColor, spinnerColor: sheetTheme.secondaryTextColor, badgeTextColor: sheetTheme.controlAccentColor, badgeImage: nil, cornersImage: generateStretchableFilledCircleImage(diameter: 11.0, color: nil, strokeColor: nil, strokeWidth: nil, backgroundColor: sheetTheme.opaqueItemBackgroundColor))
}
@@ -0,0 +1,122 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import LegacyComponents
import TelegramPresentationData
import LegacyUI
import AccountContext
import SaveToCameraRoll
public func presentLegacyAvatarPicker(holder: Atomic<NSObject?>, signup: Bool, theme: PresentationTheme, present: (ViewController, Any?) -> Void, openCurrent: (() -> Void)?, completion: @escaping (UIImage) -> Void, videoCompletion: @escaping (UIImage, Any?, TGVideoEditAdjustments?) -> Void = { _, _, _ in}) {
let legacyController = LegacyController(presentation: .custom, theme: theme)
legacyController.statusBar.statusBarStyle = .Ignore
let emptyController = LegacyEmptyController(context: legacyController.context)!
let navigationController = makeLegacyNavigationController(rootController: emptyController)
navigationController.setNavigationBarHidden(true, animated: false)
navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0)
legacyController.bind(controller: navigationController)
present(legacyController, nil)
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: false, hasDeleteButton: false, hasViewButton: openCurrent != nil, personalPhoto: true, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: signup, forum: false, title: nil, isSuggesting: false)!
let _ = holder.swap(mixin)
mixin.didFinishWithImage = { image in
guard let image = image else {
return
}
completion(image)
}
mixin.didFinishWithVideo = { image, asset, adjustments in
guard let image = image else {
return
}
videoCompletion(image, asset, adjustments)
}
mixin.didFinishWithView = {
openCurrent?()
}
mixin.didDismiss = { [weak legacyController] in
let _ = holder.swap(nil)
legacyController?.dismiss()
}
let menuController = mixin.present()
if let menuController = menuController {
menuController.customRemoveFromParentViewController = { [weak legacyController] in
legacyController?.dismiss()
}
}
}
public func legacyAvatarEditor(context: AccountContext, media: AnyMediaReference, transitionView: UIView?, senderName: String? = nil, present: @escaping (ViewController, Any?) -> Void, imageCompletion: @escaping (UIImage) -> Void, videoCompletion: @escaping (UIImage, URL, TGVideoEditAdjustments) -> Void) {
let isVideo = !((media.media as? TelegramMediaImage)?.videoRepresentations.isEmpty ?? true)
let imageSignal = fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: media, forceVideo: false)
|> map { (value, _) -> (UIImage?, Bool) in
if case let .data(data) = value, data.complete {
return (UIImage(contentsOfFile: data.path), true)
} else {
return (nil, false)
}
}
let videoSignal = isVideo ? fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: media, forceVideo: true)
|> map { (value, isImage) -> (URL?, Bool) in
if case let .data(data) = value, data.complete && !isImage {
return (URL(fileURLWithPath: data.path), true)
} else {
return (nil, false)
}
} : .single((nil, true))
let signals = combineLatest(queue: Queue.mainQueue(),
imageSignal,
videoSignal
)
|> filter { image, video in
return image.1 && video.1
}
let _ = signals.start(next: { image, video in
if image.0 == nil && video.0 == nil {
return
}
let paintStickersContext = LegacyPaintStickersContext(context: context)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil)
legacyController.blocksBackgroundWhenInOverlay = true
legacyController.acceptsFocusWhenInOverlay = true
legacyController.statusBar.statusBarStyle = .Ignore
legacyController.controllerLoaded = { [weak legacyController] in
legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true
}
let emptyController = LegacyEmptyController(context: legacyController.context)!
emptyController.navigationBarShouldBeHidden = true
let navigationController = makeLegacyNavigationController(rootController: emptyController)
navigationController.setNavigationBarHidden(true, animated: false)
legacyController.bind(controller: navigationController)
legacyController.enableSizeClassSignal = true
present(legacyController, nil)
TGPhotoVideoEditor.present(with: legacyController.context, parentController: emptyController, image: image.0, video: video.0, stickersContext: paintStickersContext, transitionView: transitionView, senderName: senderName, didFinishWithImage: { image in
if let image = image {
imageCompletion(image)
}
}, didFinishWithVideo: { image, url, adjustments in
if let image = image, let url = url, let adjustments = adjustments {
videoCompletion(image, url, adjustments)
}
}, dismissed: { [weak legacyController] in
legacyController?.dismiss()
})
})
}
@@ -0,0 +1,115 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import LegacyUI
private class DocumentPickerViewController: UIDocumentPickerViewController {
var forceDarkTheme = false
var didDisappear: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
if #available(iOS 13.0, *), self.forceDarkTheme {
self.overrideUserInterfaceStyle = .dark
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.didDisappear?()
}
}
private final class LegacyICloudFileController: LegacyController, UIDocumentPickerDelegate {
let completion: ([URL]) -> Void
init(presentation: LegacyControllerPresentation, theme: PresentationTheme?, completion: @escaping ([URL]) -> Void) {
self.completion = completion
super.init(presentation: presentation, theme: theme)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
self.completion([])
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
self.completion(urls)
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) {
self.completion([url])
}
}
public enum LegacyICloudFilePickerMode {
case `default`
case `import`
case `export`
var documentPickerMode: UIDocumentPickerMode {
switch self {
case .default:
return .open
case .import:
return .import
case .export:
return .exportToService
}
}
}
public func legacyICloudFilePicker(theme: PresentationTheme, mode: LegacyICloudFilePickerMode = .default, url: URL? = nil, documentTypes: [String] = ["public.item"], forceDarkTheme: Bool = false, dismissed: @escaping () -> Void = {}, completion: @escaping ([URL]) -> Void) -> ViewController {
var dismissImpl: (() -> Void)?
let legacyController = LegacyICloudFileController(presentation: .modal(animateIn: true), theme: theme, completion: { urls in
dismissImpl?()
completion(urls)
})
legacyController.statusBar.statusBarStyle = .Black
let controller: DocumentPickerViewController
if case .export = mode, let url {
if #available(iOS 14.0, *) {
controller = DocumentPickerViewController(forExporting: [url], asCopy: true)
} else {
controller = DocumentPickerViewController(url: url, in: mode.documentPickerMode)
}
} else {
controller = DocumentPickerViewController(documentTypes: documentTypes, in: mode.documentPickerMode)
}
controller.forceDarkTheme = forceDarkTheme || theme.overallDarkAppearance
controller.didDisappear = {
dismissImpl?()
}
controller.delegate = legacyController
if #available(iOSApplicationExtension 11.0, iOS 11.0, *), case .default = mode {
controller.allowsMultipleSelection = true
}
legacyController.presentationCompleted = { [weak legacyController] in
if let legacyController = legacyController {
if let window = legacyController.view.window {
controller.popoverPresentationController?.sourceView = window
controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0))
window.rootViewController?.present(controller, animated: true)
legacyController.presentationCompleted = nil
}
}
}
dismissImpl = { [weak legacyController] in
if let legacyController = legacyController {
legacyController.dismiss()
}
dismissed()
}
legacyController.bind(controller: UIViewController())
return legacyController
}
@@ -0,0 +1,108 @@
import Foundation
import UIKit
import TelegramCore
import LegacyComponents
import SwiftSignalKit
import AccountContext
public class VideoConversionWatcher: TGMediaVideoFileWatcher {
private let update: (String, Int) -> Void
private var path: String?
public init(update: @escaping (String, Int) -> Void) {
self.update = update
super.init()
}
override public func setup(withFileURL fileURL: URL!) {
self.path = fileURL?.path
super.setup(withFileURL: fileURL)
}
override public func fileUpdated(_ completed: Bool) -> Any! {
if let path = self.path {
var value = stat()
if stat(path, &value) == 0 {
self.update(path, Int(value.st_size))
}
}
return super.fileUpdated(completed)
}
}
public final class LegacyLiveUploadInterfaceResult: NSObject {
public let id: Int64
init(id: Int64) {
self.id = id
super.init()
}
}
public final class LegacyLiveUploadInterface: VideoConversionWatcher, TGLiveUploadInterface {
private let context: AccountContext
private let id: Int64
private var path: String?
private var size: Int?
private let data = Promise<EngineMediaResource.ResourceData>()
private let dataValue = Atomic<EngineMediaResource.ResourceData?>(value: nil)
public init(context: AccountContext) {
self.context = context
self.id = Int64.random(in: Int64.min ... Int64.max)
var updateImpl: ((String, Int) -> Void)?
super.init(update: { path, size in
updateImpl?(path, size)
})
updateImpl = { [weak self] path, size in
if let strongSelf = self {
if strongSelf.path == nil {
strongSelf.path = path
strongSelf.context.engine.resources.preUpload(id: strongSelf.id, encrypt: false, tag: nil, source: strongSelf.data.get())
}
strongSelf.size = size
let result = strongSelf.dataValue.modify { dataValue in
if let dataValue = dataValue, dataValue.isComplete {
return EngineMediaResource.ResourceData(path: path, availableSize: Int64(size), isComplete: true)
} else {
return EngineMediaResource.ResourceData(path: path, availableSize: Int64(size), isComplete: false)
}
}
if let result = result {
strongSelf.data.set(.single(result))
}
}
}
}
deinit {
}
override public func fileUpdated(_ completed: Bool) -> Any! {
let _ = super.fileUpdated(completed)
if completed {
let result = self.dataValue.modify { dataValue in
if let dataValue = dataValue {
return EngineMediaResource.ResourceData(path: dataValue.path, availableSize: dataValue.availableSize, isComplete: true)
} else {
return nil
}
}
if let result = result {
self.data.set(.single(result))
return LegacyLiveUploadInterfaceResult(id: self.id)
} else {
return nil
}
} else {
return nil
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,740 @@
import LegacyComponents
import UIKit
import Display
import Postbox
import SwiftSignalKit
import TelegramCore
import AccountContext
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import YuvConversion
import StickerResources
import SolidRoundedButtonNode
import MediaEditor
import DrawingUI
import TelegramPresentationData
import AnimatedCountLabelNode
import CoreMedia
protocol LegacyPaintEntity {
var position: CGPoint { get }
var scale: CGFloat { get }
var angle: CGFloat { get }
var baseSize: CGSize? { get }
var mirrored: Bool { get }
func image(for time: CMTime, fps: Int, completion: @escaping (CIImage?) -> Void)
}
private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, tintColor: UIColor?) -> CIImage? {
let calculatedBytesPerRow = (4 * Int(width) + 31) & (~31)
assert(bytesPerRow == calculatedBytesPerRow)
let image = generateImagePixel(CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, pixelGenerator: { _, pixelData, bytesPerRow in
switch type {
case .yuva:
data.withUnsafeBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
decodeYUVAToRGBA(bytes, pixelData, Int32(width), Int32(height), Int32(bytesPerRow))
}
case .argb:
data.withUnsafeBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
memcpy(pixelData, bytes, data.count)
}
case .dct:
break
}
})
if var image = image {
if let tintColor, let tintedImage = generateTintedImage(image: image, color: tintColor) {
image = tintedImage
}
return CIImage(image: image)
} else {
return nil
}
}
private class LegacyPaintStickerEntity: LegacyPaintEntity {
var position: CGPoint {
return self.entity.position
}
var scale: CGFloat {
return self.entity.scale
}
var angle: CGFloat {
return self.entity.rotation
}
var baseSize: CGSize? {
return self.entity.baseSize
}
var mirrored: Bool {
return self.entity.mirrored
}
let postbox: Postbox
let file: TelegramMediaFile?
let entity: DrawingStickerEntity
let animated: Bool
let durationPromise = Promise<Double>()
var source: AnimatedStickerNodeSource?
var frameQueue = Promise<QueueLocalObject<AnimatedStickerFrameQueue>?>()
var frameCount: Int?
var frameRate: Int?
var totalDuration: Double?
let queue = Queue()
let disposables = DisposableSet()
let imagePromise = Promise<UIImage>()
init(postbox: Postbox, entity: DrawingStickerEntity) {
self.postbox = postbox
self.entity = entity
self.animated = entity.isAnimated
switch entity.content {
case let .file(fileReference, _):
let file = fileReference.media
self.file = file
if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" {
self.source = AnimatedStickerResourceSource(postbox: postbox, resource: file.resource, isVideo: file.isVideoSticker || file.mimeType == "video/webm")
if let source = self.source {
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384, height: 384))
self.disposables.add((source.cachedDataPath(width: Int(fittedDimensions.width), height: Int(fittedDimensions.height))
|> deliverOn(self.queue)).start(next: { [weak self] path, complete in
if let strongSelf = self, complete {
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
let queue = strongSelf.queue
let frameSource = AnimatedStickerCachedFrameSource(queue: queue, data: data, complete: complete, notifyUpdated: {})!
strongSelf.frameCount = frameSource.frameCount
strongSelf.frameRate = frameSource.frameRate
let duration = Double(frameSource.frameCount) / Double(frameSource.frameRate)
strongSelf.totalDuration = duration
strongSelf.durationPromise.set(.single(duration))
let frameQueue = QueueLocalObject<AnimatedStickerFrameQueue>(queue: queue, generate: {
return AnimatedStickerFrameQueue(queue: queue, length: 1, source: frameSource)
})
strongSelf.frameQueue.set(.single(frameQueue))
}
}
}))
}
} else {
self.disposables.add((chatMessageSticker(postbox: self.postbox, userLocation: .other, file: file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false)
|> deliverOn(self.queue)).start(next: { [weak self] generator in
if let strongSelf = self {
let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: entity.baseSize, boundingSize: entity.baseSize, intrinsicInsets: UIEdgeInsets()))
let image = context?.generateImage()
if let image = image {
strongSelf.imagePromise.set(.single(image))
}
}
}))
}
case let .image(image, _):
self.file = nil
self.imagePromise.set(.single(image))
case .animatedImage, .video, .dualVideoReference, .message, .gift:
self.file = nil
}
}
deinit {
self.disposables.dispose()
}
var duration: Signal<Double, NoError> {
return self.durationPromise.get()
}
var currentFrameIndex: Int?
var cachedCIImage: CIImage?
func image(for time: CMTime, fps: Int, completion: @escaping (CIImage?) -> Void) {
if self.animated {
let currentTime = CMTimeGetSeconds(time)
var tintColor: UIColor?
if let file = self.file, file.isCustomTemplateEmoji {
tintColor = .white
}
self.disposables.add((self.frameQueue.get()
|> take(1)
|> deliverOn(self.queue)).start(next: { [weak self] frameQueue in
guard let strongSelf = self else {
completion(nil)
return
}
guard let frameQueue = frameQueue, let duration = strongSelf.totalDuration, let frameCount = strongSelf.frameCount else {
completion(nil)
return
}
let relativeTime = currentTime - floor(currentTime / duration) * duration
var t = relativeTime / duration
t = max(0.0, t)
t = min(1.0, t)
let startFrame: Double = 0
let endFrame = Double(frameCount)
let frameOffset = Int(Double(startFrame) * (1.0 - t) + Double(endFrame - 1) * t)
let lowerBound: Int = 0
let upperBound = frameCount - 1
let frameIndex = max(lowerBound, min(upperBound, frameOffset))
let currentFrameIndex = strongSelf.currentFrameIndex
if currentFrameIndex != frameIndex {
let previousFrameIndex = currentFrameIndex
strongSelf.currentFrameIndex = frameIndex
var delta = 1
if let previousFrameIndex = previousFrameIndex {
delta = max(1, frameIndex - previousFrameIndex)
}
let maybeFrame = frameQueue.syncWith { frameQueue -> AnimatedStickerFrame? in
var frame: AnimatedStickerFrame?
for i in 0 ..< delta {
frame = frameQueue.take(draw: i == delta - 1)
}
return frame
}
if let frame = maybeFrame {
let image = render(width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, tintColor: tintColor)
completion(image)
strongSelf.cachedCIImage = image
} else {
completion(nil)
}
frameQueue.with { frameQueue in
frameQueue.generateFramesIfNeeded()
}
} else {
completion(strongSelf.cachedCIImage)
}
}))
} else {
self.queue.async {
var image: CIImage?
if let cachedImage = self.cachedCIImage {
image = cachedImage
completion(image)
} else {
let _ = (self.imagePromise.get()
|> take(1)
|> deliverOn(self.queue)).start(next: { [weak self] image in
if let strongSelf = self {
strongSelf.cachedCIImage = CIImage(image: image)
completion(strongSelf.cachedCIImage)
}
})
}
}
}
}
}
private class LegacyPaintTextEntity: LegacyPaintEntity {
var position: CGPoint {
return self.entity.position
}
var scale: CGFloat {
return self.entity.scale
}
var angle: CGFloat {
return self.entity.rotation
}
var baseSize: CGSize? {
return nil
}
var mirrored: Bool {
return false
}
let entity: DrawingTextEntity
init(entity: DrawingTextEntity) {
self.entity = entity
}
var cachedCIImage: CIImage?
func image(for time: CMTime, fps: Int, completion: @escaping (CIImage?) -> Void) {
var image: CIImage?
if let cachedImage = self.cachedCIImage {
image = cachedImage
} else if let renderImage = entity.renderImage {
image = CIImage(image: renderImage)
self.cachedCIImage = image
}
completion(image)
}
}
private class LegacyPaintSimpleShapeEntity: LegacyPaintEntity {
var position: CGPoint {
return self.entity.position
}
var scale: CGFloat {
return 1.0
}
var angle: CGFloat {
return self.entity.rotation
}
var baseSize: CGSize? {
return self.entity.size
}
var mirrored: Bool {
return false
}
let entity: DrawingSimpleShapeEntity
init(entity: DrawingSimpleShapeEntity) {
self.entity = entity
}
var cachedCIImage: CIImage?
func image(for time: CMTime, fps: Int, completion: @escaping (CIImage?) -> Void) {
var image: CIImage?
if let cachedImage = self.cachedCIImage {
image = cachedImage
} else if let renderImage = entity.renderImage {
image = CIImage(image: renderImage)
self.cachedCIImage = image
}
completion(image)
}
}
private class LegacyPaintBubbleEntity: LegacyPaintEntity {
var position: CGPoint {
return self.entity.position
}
var scale: CGFloat {
return 1.0
}
var angle: CGFloat {
return self.entity.rotation
}
var baseSize: CGSize? {
return self.entity.size
}
var mirrored: Bool {
return false
}
let entity: DrawingBubbleEntity
init(entity: DrawingBubbleEntity) {
self.entity = entity
}
var cachedCIImage: CIImage?
func image(for time: CMTime, fps: Int, completion: @escaping (CIImage?) -> Void) {
var image: CIImage?
if let cachedImage = self.cachedCIImage {
image = cachedImage
} else if let renderImage = entity.renderImage {
image = CIImage(image: renderImage)
self.cachedCIImage = image
}
completion(image)
}
}
private class LegacyPaintVectorEntity: LegacyPaintEntity {
var position: CGPoint {
return CGPoint(x: self.entity.drawingSize.width * 0.5, y: self.entity.drawingSize.height * 0.5)
}
var scale: CGFloat {
return 1.0
}
var angle: CGFloat {
return 0.0
}
var baseSize: CGSize? {
return self.entity.drawingSize
}
var mirrored: Bool {
return false
}
let entity: DrawingVectorEntity
init(entity: DrawingVectorEntity) {
self.entity = entity
}
var cachedCIImage: CIImage?
func image(for time: CMTime, fps: Int, completion: @escaping (CIImage?) -> Void) {
var image: CIImage?
if let cachedImage = self.cachedCIImage {
image = cachedImage
} else if let renderImage = entity.renderImage {
image = CIImage(image: renderImage)
self.cachedCIImage = image
}
completion(image)
}
}
public final class LegacyPaintEntityRenderer: NSObject, TGPhotoPaintEntityRenderer {
private let postbox: Postbox?
private let queue = Queue()
private let entities: [LegacyPaintEntity]
private let originalSize: CGSize
private let cropRect: CGRect?
private let isAvatar: Bool
public init(postbox: Postbox?, adjustments: TGMediaEditAdjustments) {
self.postbox = postbox
self.originalSize = adjustments.originalSize
self.cropRect = adjustments.cropRect.isEmpty ? nil : adjustments.cropRect
self.isAvatar = ((adjustments as? TGVideoEditAdjustments)?.documentId ?? 0) != 0
var renderEntities: [LegacyPaintEntity] = []
if let paintingData = adjustments.paintingData, let entitiesData = paintingData.entitiesData {
let entities = decodeDrawingEntities(data: entitiesData)
for entity in entities {
if let sticker = entity as? DrawingStickerEntity, let postbox {
renderEntities.append(LegacyPaintStickerEntity(postbox: postbox, entity: sticker))
} else if let text = entity as? DrawingTextEntity {
renderEntities.append(LegacyPaintTextEntity(entity: text))
if let renderSubEntities = text.renderSubEntities, let postbox {
for entity in renderSubEntities {
if let entity = entity as? DrawingStickerEntity {
renderEntities.append(LegacyPaintStickerEntity(postbox: postbox, entity: entity))
}
}
}
} else if let simpleShape = entity as? DrawingSimpleShapeEntity {
renderEntities.append(LegacyPaintSimpleShapeEntity(entity: simpleShape))
} else if let bubble = entity as? DrawingBubbleEntity {
renderEntities.append(LegacyPaintBubbleEntity(entity: bubble))
} else if let vector = entity as? DrawingVectorEntity {
renderEntities.append(LegacyPaintVectorEntity(entity: vector))
}
}
}
self.entities = renderEntities
super.init()
}
deinit {
}
public func duration() -> Signal<Double, NoError> {
var durations: [Signal<Double, NoError>] = []
for entity in self.entities {
if let sticker = entity as? LegacyPaintStickerEntity, sticker.animated {
durations.append(sticker.duration)
}
}
func gcd(_ a: Int64, _ b: Int64) -> Int64 {
let remainder = a % b
if remainder != 0 {
return gcd(b, remainder)
} else {
return b
}
}
func lcm(_ x: Int64, _ y: Int64) -> Int64 {
let x = max(x, 1)
let y = max(y, 1)
return x / gcd(x, y) * y
}
return combineLatest(durations)
|> map { durations in
var result: Double
let minDuration: Double = 3.0
if durations.count > 1 {
let reduced = durations.reduce(1.0) { lhs, rhs -> Double in
return Double(lcm(Int64(lhs * 100.0), Int64(rhs * 100.0)))
}
result = min(6.0, Double(reduced) / 10.0)
} else if let duration = durations.first {
result = duration
} else {
result = minDuration
}
if result < minDuration && !self.isAvatar {
if result > 0 {
result = result * ceil(minDuration / result)
} else {
result = minDuration
}
}
return result
}
}
public func entities(for time: CMTime, fps: Int, size: CGSize, completion: @escaping ([CIImage]) -> Void) {
let entities = self.entities
let maxSide = max(size.width, size.height)
let paintingScale = maxSide / 1920.0
self.queue.async {
if entities.isEmpty {
completion([])
} else {
let count = Atomic<Int>(value: 1)
let images = Atomic<[(CIImage, Int)]>(value: [])
let maybeFinalize = {
let count = count.modify { current -> Int in
return current - 1
}
if count == 0 {
let sortedImages = images.with({ $0 }).sorted(by: { $0.1 < $1.1 }).map({ $0.0 })
completion(sortedImages)
}
}
var i = 0
for entity in entities {
let _ = count.modify { current -> Int in
return current + 1
}
let index = i
entity.image(for: time, fps: fps, completion: { image in
if var image = image {
var transform = CGAffineTransform(translationX: -image.extent.midX, y: -image.extent.midY)
image = image.transformed(by: transform)
var scale = entity.scale * paintingScale
if let baseSize = entity.baseSize {
scale *= baseSize.width / image.extent.size.width
}
transform = CGAffineTransform(translationX: entity.position.x * paintingScale, y: size.height - entity.position.y * paintingScale)
transform = transform.rotated(by: CGFloat.pi * 2.0 - entity.angle)
transform = transform.scaledBy(x: scale, y: scale)
if entity.mirrored {
transform = transform.scaledBy(x: -1.0, y: 1.0)
}
image = image.transformed(by: transform)
let _ = images.modify { current in
var updated = current
updated.append((image, index))
return updated
}
}
maybeFinalize()
})
i += 1
}
maybeFinalize()
}
}
}
}
public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersContext {
public var captionPanelView: (() -> TGCaptionPanelView?)?
public var editCover: ((CGSize, @escaping (UIImage) -> Void) -> Void)?
private let context: AccountContext
public init(context: AccountContext) {
self.context = context
}
class LegacyDrawingAdapter: NSObject, TGPhotoDrawingAdapter {
let drawingView: TGPhotoDrawingView
let drawingEntitiesView: TGPhotoDrawingEntitiesView
let selectionContainerView: UIView
let contentWrapperView: UIView
let interfaceController: TGPhotoDrawingInterfaceController
init(context: AccountContext, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?) {
let interfaceController = DrawingScreen(context: context, size: size, originalSize: originalSize, isVideo: isVideo, isAvatar: isAvatar, drawingView: nil, entitiesView: entitiesView, selectionContainerView: nil)
self.interfaceController = interfaceController
self.drawingView = interfaceController.drawingView
self.drawingEntitiesView = interfaceController.entitiesView
self.selectionContainerView = interfaceController.selectionContainerView
self.contentWrapperView = interfaceController.contentWrapperView
super.init()
}
}
public func drawingAdapter(_ size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?) -> TGPhotoDrawingAdapter {
return LegacyDrawingAdapter(context: self.context, size: size, originalSize: originalSize, isVideo: isVideo, isAvatar: isAvatar, entitiesView: entitiesView)
}
public func solidRoundedButton(_ title: String, action: @escaping () -> Void) -> UIView & TGPhotoSolidRoundedButtonView {
let theme = SolidRoundedButtonTheme(theme: self.context.sharedContext.currentPresentationData.with { $0 }.theme)
let button = SolidRoundedButtonView(title: title, theme: theme, height: 50.0, cornerRadius: 10.0)
button.pressed = action
return button
}
public func sendStarsButtonAction(_ action: @escaping () -> Void) -> any UIView & TGPhotoSendStarsButtonView {
let button = SendStarsButtonView()
button.pressed = action
return button
}
public func drawingEntitiesView(with size: CGSize) -> UIView & TGPhotoDrawingEntitiesView {
let view = DrawingEntitiesView(context: self.context, size: size)
return view
}
}
private class SendStarsButtonView: HighlightTrackingButton, TGPhotoSendStarsButtonView {
private let backgroundView: UIView
private let textNode: ImmediateAnimatedCountLabelNode
fileprivate var pressed: (() -> Void)?
override init(frame: CGRect) {
self.backgroundView = UIView()
self.backgroundView.isUserInteractionEnabled = false
self.textNode = ImmediateAnimatedCountLabelNode()
self.textNode.isUserInteractionEnabled = false
super.init(frame: frame)
self.addSubview(self.backgroundView)
self.addSubview(self.textNode.view)
self.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if highlighted {
self.backgroundView.layer.removeAnimation(forKey: "opacity")
self.backgroundView.alpha = 0.4
self.textNode.layer.removeAnimation(forKey: "opacity")
self.textNode.alpha = 0.4
} else {
self.backgroundView.alpha = 1.0
self.backgroundView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
self.textNode.alpha = 1.0
self.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
self.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
deinit {
print()
}
@objc private func buttonPressed() {
self.pressed?()
}
func updateFrame(_ frame: CGRect) {
let transition: ContainedViewLayoutTransition
if self.frame.width.isZero {
transition = .immediate
} else {
transition = .animated(duration: 0.4, curve: .spring)
}
transition.updateFrame(view: self, frame: frame)
}
func updateCount(_ count: Int64) -> CGSize {
let text = "\(count)"
let transition: ContainedViewLayoutTransition
if self.backgroundView.frame.width.isZero {
transition = .immediate
} else {
transition = .animated(duration: 0.4, curve: .spring)
}
var segments: [AnimatedCountLabelNode.Segment] = []
let font = Font.with(size: 17.0, design: .round, weight: .semibold, traits: .monospacedNumbers)
let badgeString = NSMutableAttributedString(string: "⭐️ ", font: font, textColor: .white)
if let range = badgeString.string.range(of: "⭐️") {
badgeString.addAttribute(.attachment, value: PresentationResourcesChat.chatPlaceholderStarIcon(defaultDarkPresentationTheme)!, range: NSRange(range, in: badgeString.string))
badgeString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: badgeString.string))
}
segments.append(.text(0, badgeString))
for char in text {
if let intValue = Int(String(char)) {
segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: .white)))
}
}
self.textNode.segments = segments
let buttonInset: CGFloat = 14.0
let textSize = self.textNode.updateLayout(size: CGSize(width: 100.0, height: 100.0), animated: transition.isAnimated)
let width = textSize.width + buttonInset * 2.0
let buttonSize = CGSize(width: width, height: 45.0)
let titleOffset: CGFloat = 0.0
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - textSize.width) / 2.0) + titleOffset, y: floorToScreenPixels((buttonSize.height - textSize.height) / 2.0)), size: textSize))
let backgroundSize = CGSize(width: width - 11.0, height: 33.0)
transition.updateFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - backgroundSize.width) / 2.0), y: floorToScreenPixels((buttonSize.height - backgroundSize.height) / 2.0)), size: backgroundSize))
self.backgroundView.layer.cornerRadius = backgroundSize.height / 2.0
self.backgroundView.backgroundColor = UIColor(rgb: 0x0088ff)
return buttonSize;
}
}
#if SWIFT_PACKAGE
extension SolidRoundedButtonView: TGPhotoSolidRoundedButtonView {
public func updateWidth(_ width: CGFloat) {
let _ = self.updateLayout(width: width, transition: .immediate)
}
}
#else
extension SolidRoundedButtonView: @retroactive TGPhotoSolidRoundedButtonView {
public func updateWidth(_ width: CGFloat) {
let _ = self.updateLayout(width: width, transition: .immediate)
}
}
#endif
@@ -0,0 +1,124 @@
import Foundation
import UIKit
import Display
import SSignalKit
import SwiftSignalKit
import TelegramCore
import LegacyComponents
import TelegramPresentationData
import DeviceAccess
import AccountContext
import LocalMediaResources
import Photos
public func legacyWallpaperPicker(context: AccountContext, presentationData: PresentationData, subject: DeviceAccessMediaLibrarySubject = .wallpaper) -> Signal<(LegacyComponentsContext) -> TGMediaAssetsController, Void> {
return Signal { subscriber in
let intent = TGMediaAssetsControllerSetCustomWallpaperIntent
DeviceAccess.authorizeAccess(to: .mediaLibrary(subject), presentationData: presentationData, present: context.sharedContext.presentGlobalController, openSettings: context.sharedContext.applicationBindings.openSettings, { value in
if !value {
subscriber.putError(Void())
return
}
if TGMediaAssetsLibrary.authorizationStatus() == TGMediaLibraryAuthorizationStatusNotDetermined {
TGMediaAssetsLibrary.requestAuthorization(for: TGMediaAssetAnyType, completion: { (status, group) in
if !LegacyComponentsGlobals.provider().accessChecker().checkPhotoAuthorizationStatus(for: TGPhotoAccessIntentRead, alertDismissCompletion: nil) {
subscriber.putError(Void())
} else {
Queue.mainQueue().async {
subscriber.putNext({ context in
let controller = TGMediaAssetsController(context: context, assetGroup: group, intent: intent, recipientName: nil, saveEditedPhotos: false, allowGrouping: false, selectionLimit: 1)
return controller!
})
subscriber.putCompletion()
}
}
})
} else {
subscriber.putNext({ context in
let controller = TGMediaAssetsController(context: context, assetGroup: nil, intent: intent, recipientName: nil, saveEditedPhotos: false, allowGrouping: false, selectionLimit: 1)
return controller!
})
subscriber.putCompletion()
}
})
return ActionDisposable {
}
}
}
public class LegacyWallpaperItem: NSObject, TGMediaEditableItem, TGMediaSelectableItem {
public var isVideo: Bool {
return false
}
public var uniqueIdentifier: String! {
return self.asset.localIdentifier
}
let asset: PHAsset
let screenImage: UIImage
private(set) var thumbnailResource: TelegramMediaResource?
private(set) var imageResource: TelegramMediaResource?
let dimensions: CGSize
public init(asset: PHAsset, screenImage: UIImage, dimensions: CGSize) {
self.asset = asset
self.screenImage = screenImage
self.dimensions = dimensions
}
public var originalSize: CGSize {
return self.dimensions
}
public func thumbnailImageSignal() -> SSignal! {
return SSignal.complete()
// return SSignal(generator: { subscriber -> SDisposable? in
// let disposable = self.thumbnailImage.start(next: { image in
// subscriber.putNext(image)
// subscriber.putCompletion()
// })
//
// return SBlockDisposable(block: {
// disposable.dispose()
// })
// })
}
public func screenImageSignal(_ position: TimeInterval) -> SSignal! {
return SSignal.single(self.screenImage)
}
public var originalImage: Signal<UIImage, NoError> {
return fetchPhotoLibraryImage(localIdentifier: self.asset.localIdentifier, thumbnail: false)
|> filter { value in
return !(value?.1 ?? true)
}
|> mapToSignal { result -> Signal<UIImage, NoError> in
if let result = result {
return .single(result.0)
} else {
return .complete()
}
}
}
public func originalImageSignal(_ position: TimeInterval) -> SSignal! {
return SSignal(generator: { subscriber -> SDisposable? in
let disposable = self.originalImage.start(next: { image in
subscriber.putNext(image)
if !image.degraded() {
subscriber.putCompletion()
}
})
return SBlockDisposable(block: {
disposable.dispose()
})
})
}
}