Merge commit '5e5f19e6e98298c95fefe548ec0de31752af9c55'

This commit is contained in:
Ali 2023-07-14 14:20:51 +04:00
commit a0032f4fee
19 changed files with 714 additions and 95 deletions

View File

@ -2062,7 +2062,7 @@
"StickerPack.Share" = "Share"; "StickerPack.Share" = "Share";
"StickerPack.Send" = "Send Sticker"; "StickerPack.Send" = "Send Sticker";
"StickerPack.Select" = "Select Sticker"; "StickerPack.AddSticker" = "Add Sticker";
"StickerPack.RemoveStickerCount_1" = "Remove 1 Sticker"; "StickerPack.RemoveStickerCount_1" = "Remove 1 Sticker";
"StickerPack.RemoveStickerCount_2" = "Remove 2 Stickers"; "StickerPack.RemoveStickerCount_2" = "Remove 2 Stickers";

View File

@ -57,6 +57,37 @@ enum DeviceModel: CaseIterable, Equatable {
.iPodTouch5, .iPodTouch5,
.iPodTouch6, .iPodTouch6,
.iPodTouch7, .iPodTouch7,
.iPhone,
.iPhone3G,
.iPhone3GS,
.iPhone4,
.iPhone4S,
.iPhone5,
.iPhone5C,
.iPhone5S,
.iPhone6,
.iPhone6Plus,
.iPhone6S,
.iPhone6SPlus,
.iPhoneSE,
.iPhone7,
.iPhone7Plus,
.iPhone8,
.iPhone8Plus,
.iPhoneX,
.iPhoneXS,
.iPhoneXR,
.iPhone11,
.iPhone11Pro,
.iPhone11ProMax,
.iPhone12,
.iPhone12Mini,
.iPhone12Pro,
.iPhone12ProMax,
.iPhone13,
.iPhone13Mini,
.iPhone13Pro,
.iPhone13ProMax,
.iPhone14, .iPhone14,
.iPhone14Plus, .iPhone14Plus,
.iPhone14Pro, .iPhone14Pro,
@ -72,9 +103,40 @@ enum DeviceModel: CaseIterable, Equatable {
case iPodTouch6 case iPodTouch6
case iPodTouch7 case iPodTouch7
case iPhone
case iPhone3G
case iPhone3GS
case iPhone4
case iPhone4S
case iPhone5
case iPhone5C
case iPhone5S
case iPhone6
case iPhone6Plus
case iPhone6S
case iPhone6SPlus
case iPhoneSE
case iPhone7
case iPhone7Plus
case iPhone8
case iPhone8Plus
case iPhoneX case iPhoneX
case iPhoneXS case iPhoneXS
case iPhoneXSMax
case iPhoneXR
case iPhone11
case iPhone11Pro
case iPhone11ProMax
case iPhoneSE2ndGen
case iPhone12 case iPhone12
case iPhone12Mini case iPhone12Mini
case iPhone12Pro case iPhone12Pro
@ -85,6 +147,8 @@ enum DeviceModel: CaseIterable, Equatable {
case iPhone13Pro case iPhone13Pro
case iPhone13ProMax case iPhone13ProMax
case iPhoneSE3rdGen
case iPhone14 case iPhone14
case iPhone14Plus case iPhone14Plus
case iPhone14Pro case iPhone14Pro
@ -108,10 +172,56 @@ enum DeviceModel: CaseIterable, Equatable {
return ["iPod7,1"] return ["iPod7,1"]
case .iPodTouch7: case .iPodTouch7:
return ["iPod9,1"] return ["iPod9,1"]
case .iPhone:
return ["iPhone1,1"]
case .iPhone3G:
return ["iPhone1,2"]
case .iPhone3GS:
return ["iPhone2,1"]
case .iPhone4:
return ["iPhone3,1", "iPhone3,2", "iPhone3,3"]
case .iPhone4S:
return ["iPhone4,1", "iPhone4,2", "iPhone4,3"]
case .iPhone5:
return ["iPhone5,1", "iPhone5,2"]
case .iPhone5C:
return ["iPhone5,3", "iPhone5,4"]
case .iPhone5S:
return ["iPhone6,1", "iPhone6,2"]
case .iPhone6:
return ["iPhone7,2"]
case .iPhone6Plus:
return ["iPhone7,1"]
case .iPhone6S:
return ["iPhone8,1"]
case .iPhone6SPlus:
return ["iPhone8,2"]
case .iPhoneSE:
return ["iPhone8,4"]
case .iPhone7:
return ["iPhone9,1", "iPhone9,3"]
case .iPhone7Plus:
return ["iPhone9,2", "iPhone9,4"]
case .iPhone8:
return ["iPhone10,1", "iPhone10,4"]
case .iPhone8Plus:
return ["iPhone10,2", "iPhone10,5"]
case .iPhoneX: case .iPhoneX:
return ["iPhone11,2"] return ["iPhone10,3", "iPhone10,6"]
case .iPhoneXS: case .iPhoneXS:
return ["iPhone11,2"]
case .iPhoneXSMax:
return ["iPhone11,4", "iPhone11,6"] return ["iPhone11,4", "iPhone11,6"]
case .iPhoneXR:
return ["iPhone11,8"]
case .iPhone11:
return ["iPhone12,1"]
case .iPhone11Pro:
return ["iPhone12,3"]
case .iPhone11ProMax:
return ["iPhone12,5"]
case .iPhoneSE2ndGen:
return ["iPhone12,8"]
case .iPhone12: case .iPhone12:
return ["iPhone13,2"] return ["iPhone13,2"]
case .iPhone12Mini: case .iPhone12Mini:
@ -128,6 +238,8 @@ enum DeviceModel: CaseIterable, Equatable {
return ["iPhone14,2"] return ["iPhone14,2"]
case .iPhone13ProMax: case .iPhone13ProMax:
return ["iPhone14,3"] return ["iPhone14,3"]
case .iPhoneSE3rdGen:
return ["iPhone14,6"]
case .iPhone14: case .iPhone14:
return ["iPhone14,7"] return ["iPhone14,7"]
case .iPhone14Plus: case .iPhone14Plus:
@ -157,10 +269,56 @@ enum DeviceModel: CaseIterable, Equatable {
return "iPod touch 6G" return "iPod touch 6G"
case .iPodTouch7: case .iPodTouch7:
return "iPod touch 7G" return "iPod touch 7G"
case .iPhone:
return "iPhone"
case .iPhone3G:
return "iPhone 3G"
case .iPhone3GS:
return "iPhone 3GS"
case .iPhone4:
return "iPhone 4"
case .iPhone4S:
return "iPhone 4S"
case .iPhone5:
return "iPhone 5"
case .iPhone5C:
return "iPhone 5C"
case .iPhone5S:
return "iPhone 5S"
case .iPhone6:
return "iPhone 6"
case .iPhone6Plus:
return "iPhone 6 Plus"
case .iPhone6S:
return "iPhone 6S"
case .iPhone6SPlus:
return "iPhone 6S Plus"
case .iPhoneSE:
return "iPhone SE"
case .iPhone7:
return "iPhone 7"
case .iPhone7Plus:
return "iPhone 7 Plus"
case .iPhone8:
return "iPhone 8"
case .iPhone8Plus:
return "iPhone 8 Plus"
case .iPhoneX: case .iPhoneX:
return "iPhone X" return "iPhone X"
case .iPhoneXS: case .iPhoneXS:
return "iPhone XS" return "iPhone XS"
case .iPhoneXSMax:
return "iPhone XS Max"
case .iPhoneXR:
return "iPhone XR"
case .iPhone11:
return "iPhone 11"
case .iPhone11Pro:
return "iPhone 11 Pro"
case .iPhone11ProMax:
return "iPhone 11 Pro Max"
case .iPhoneSE2ndGen:
return "iPhone SE (2nd gen)"
case .iPhone12: case .iPhone12:
return "iPhone 12" return "iPhone 12"
case .iPhone12Mini: case .iPhone12Mini:
@ -177,6 +335,8 @@ enum DeviceModel: CaseIterable, Equatable {
return "iPhone 13 Pro" return "iPhone 13 Pro"
case .iPhone13ProMax: case .iPhone13ProMax:
return "iPhone 13 Pro Max" return "iPhone 13 Pro Max"
case .iPhoneSE3rdGen:
return "iPhone SE (3rd gen)"
case .iPhone14: case .iPhone14:
return "iPhone 14" return "iPhone 14"
case .iPhone14Plus: case .iPhone14Plus:

View File

@ -82,6 +82,16 @@ public final class DeviceAccess {
return self.locationPromise.get() return self.locationPromise.get()
} }
private static let cameraPromise = Promise<Bool?>(nil)
static var camera: Signal<Bool?, NoError> {
return self.cameraPromise.get()
}
private static let microphonePromise = Promise<Bool?>(nil)
static var microphone: Signal<Bool?, NoError> {
return self.microphonePromise.get()
}
public static func isMicrophoneAccessAuthorized() -> Bool? { public static func isMicrophoneAccessAuthorized() -> Bool? {
return AVAudioSession.sharedInstance().recordPermission == .granted return AVAudioSession.sharedInstance().recordPermission == .granted
} }
@ -248,12 +258,72 @@ public final class DeviceAccess {
} }
} }
) )
case .camera:
return Signal { subscriber in
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .authorized:
subscriber.putNext(.allowed)
case .denied, .restricted:
subscriber.putNext(.denied)
case .notDetermined:
subscriber.putNext(.notDetermined)
@unknown default:
fatalError()
}
subscriber.putCompletion()
return EmptyDisposable
}
|> then(self.camera
|> mapToSignal { authorized -> Signal<AccessType, NoError> in
if let authorized = authorized {
return .single(authorized ? .allowed : .denied)
} else {
return .complete()
}
}
)
case .microphone:
return Signal { subscriber in
let status = AVCaptureDevice.authorizationStatus(for: .audio)
switch status {
case .authorized:
subscriber.putNext(.allowed)
case .denied, .restricted:
subscriber.putNext(.denied)
case .notDetermined:
subscriber.putNext(.notDetermined)
@unknown default:
fatalError()
}
subscriber.putCompletion()
return EmptyDisposable
}
|> then(self.microphone
|> mapToSignal { authorized -> Signal<AccessType, NoError> in
if let authorized = authorized {
return .single(authorized ? .allowed : .denied)
} else {
return .complete()
}
}
)
default: default:
return .single(.notDetermined) return .single(.notDetermined)
} }
} }
public static func authorizeAccess(to subject: DeviceAccessSubject, onlyCheck: Bool = false, registerForNotifications: ((@escaping (Bool) -> Void) -> Void)? = nil, requestSiriAuthorization: ((@escaping (Bool) -> Void) -> Void)? = nil, locationManager: LocationManager? = nil, presentationData: PresentationData? = nil, present: @escaping (ViewController, Any?) -> Void = { _, _ in }, openSettings: @escaping () -> Void = { }, displayNotificationFromBackground: @escaping (String) -> Void = { _ in }, _ completion: @escaping (Bool) -> Void = { _ in }) { public static func authorizeAccess(
to subject: DeviceAccessSubject,
onlyCheck: Bool = false,
registerForNotifications: ((@escaping (Bool) -> Void) -> Void)? = nil,
requestSiriAuthorization: ((@escaping (Bool) -> Void) -> Void)? = nil,
locationManager: LocationManager? = nil,
presentationData: PresentationData? = nil,
present: @escaping (ViewController, Any?) -> Void = { _, _ in },
openSettings: @escaping () -> Void = { },
displayNotificationFromBackground: @escaping (String) -> Void = { _ in },
_ completion: @escaping (Bool) -> Void = { _ in }) {
switch subject { switch subject {
case let .camera(cameraSubject): case let .camera(cameraSubject):
let status = AVCaptureDevice.authorizationStatus(for: .video) let status = AVCaptureDevice.authorizationStatus(for: .video)
@ -262,6 +332,7 @@ public final class DeviceAccess {
AVCaptureDevice.requestAccess(for: AVMediaType.video) { response in AVCaptureDevice.requestAccess(for: AVMediaType.video) { response in
Queue.mainQueue().async { Queue.mainQueue().async {
completion(response) completion(response)
self.cameraPromise.set(.single(response))
if !response, let presentationData = presentationData { if !response, let presentationData = presentationData {
let text: String let text: String
switch cameraSubject { switch cameraSubject {
@ -331,6 +402,7 @@ public final class DeviceAccess {
displayNotificationFromBackground(text) displayNotificationFromBackground(text)
} }
} }
self.microphonePromise.set(.single(granted))
} }
}) })
} }

View File

@ -694,6 +694,8 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
if let selectedEntityView = self.selectedEntityView, let selectionView = selectedEntityView.selectionView { if let selectedEntityView = self.selectedEntityView, let selectionView = selectedEntityView.selectionView {
if !self.hasBin { if !self.hasBin {
selectionView.handlePan(gestureRecognizer) selectionView.handlePan(gestureRecognizer)
} else if let stickerEntity = selectedEntityView.entity as? DrawingStickerEntity, case .dualVideoReference = stickerEntity.content {
selectionView.handlePan(gestureRecognizer)
} else { } else {
var isTrappedInBin = false var isTrappedInBin = false
let scale = 100.0 / selectedEntityView.bounds.size.width let scale = 100.0 / selectedEntityView.bounds.size.width

View File

@ -2930,7 +2930,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
} }
let images = imageItems as! [UIImage] let images = imageItems as! [UIImage]
if images.count == 1, let image = images.first, max(image.size.width, image.size.height) > 1.0 { if images.count == 1, let image = images.first, max(image.size.width, image.size.height) > 1.0 {
let entity = DrawingStickerEntity(content: .image(image, false)) let entity = DrawingStickerEntity(content: .image(image, .sticker))
strongSelf.node.insertEntity.invoke(entity) strongSelf.node.insertEntity.invoke(entity)
} }
} }

View File

@ -289,7 +289,7 @@ private final class StickerSelectionComponent: Component {
interaction: interaction, interaction: interaction,
inputNodeInteraction: inputNodeInteraction, inputNodeInteraction: inputNodeInteraction,
mode: mappedMode, mode: mappedMode,
stickerActionTitle: presentationData.strings.StickerPack_Select, stickerActionTitle: presentationData.strings.StickerPack_AddSticker,
trendingGifsPromise: trendingGifsPromise, trendingGifsPromise: trendingGifsPromise,
cancel: { cancel: {
}, },
@ -585,7 +585,7 @@ public class StickerPickerScreen: ViewController {
CTLineDraw(line, context) CTLineDraw(line, context)
context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y)
}) { }) {
strongSelf.controller?.completion(.image(image, false)) strongSelf.controller?.completion(.image(image, .sticker))
} }
strongSelf.controller?.dismiss(animated: true) strongSelf.controller?.dismiss(animated: true)
} }

View File

@ -261,6 +261,7 @@ func assetImage(asset: PHAsset, targetSize: CGSize, exact: Bool, deliveryMode: P
if let info = info { if let info = info {
if let cancelled = info[PHImageCancelledKey] as? Bool, cancelled { if let cancelled = info[PHImageCancelledKey] as? Bool, cancelled {
subscriber.putCompletion()
return return
} }
if let degradedValue = info[PHImageResultIsDegradedKey] as? Bool, degradedValue { if let degradedValue = info[PHImageResultIsDegradedKey] as? Bool, degradedValue {

View File

@ -125,23 +125,27 @@ final class MediaPickerGridItemNode: GridItemNode {
override init() { override init() {
self.backgroundNode = ASImageNode() self.backgroundNode = ASImageNode()
self.backgroundNode.contentMode = .scaleToFill self.backgroundNode.contentMode = .scaleToFill
self.backgroundNode.isLayerBacked = true
self.imageNode = ImageNode() self.imageNode = ImageNode()
self.imageNode.clipsToBounds = true self.imageNode.clipsToBounds = true
self.imageNode.contentMode = .scaleAspectFill self.imageNode.contentMode = .scaleAspectFill
self.imageNode.isLayerBacked = false self.imageNode.isLayerBacked = true
self.imageNode.animateFirstTransition = false self.imageNode.animateFirstTransition = false
self.gradientNode = ASImageNode() self.gradientNode = ASImageNode()
self.gradientNode.displaysAsynchronously = false self.gradientNode.displaysAsynchronously = false
self.gradientNode.displayWithoutProcessing = true self.gradientNode.displayWithoutProcessing = true
self.gradientNode.image = maskImage self.gradientNode.image = maskImage
self.gradientNode.isLayerBacked = true
self.typeIconNode = ASImageNode() self.typeIconNode = ASImageNode()
self.typeIconNode.displaysAsynchronously = false self.typeIconNode.displaysAsynchronously = false
self.typeIconNode.displayWithoutProcessing = true self.typeIconNode.displayWithoutProcessing = true
self.typeIconNode.isLayerBacked = true
self.durationNode = ImmediateTextNode() self.durationNode = ImmediateTextNode()
self.durationNode.isLayerBacked = true
self.draftNode = ImmediateTextNode() self.draftNode = ImmediateTextNode()
self.activateAreaNode = AccessibilityAreaNode() self.activateAreaNode = AccessibilityAreaNode()
@ -472,7 +476,7 @@ final class MediaPickerGridItemNode: GridItemNode {
} }
} }
let originalSignal = assetImageSignal //assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, synchronous: true) let originalSignal = assetImageSignal
let imageSignal: Signal<UIImage?, NoError> = editedSignal let imageSignal: Signal<UIImage?, NoError> = editedSignal
|> mapToSignal { result in |> mapToSignal { result in
if let result = result { if let result = result {
@ -519,18 +523,22 @@ final class MediaPickerGridItemNode: GridItemNode {
self.addSubnode(self.typeIconNode) self.addSubnode(self.typeIconNode)
self.setNeedsLayout() self.setNeedsLayout()
} }
} else if asset.mediaType == .video { }
if asset.mediaSubtypes.contains(.videoHighFrameRate) {
self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaSlomo") if asset.mediaType == .video {
} else if asset.mediaSubtypes.contains(.videoTimelapse) { if !asset.isFavorite {
self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaTimelapse") if asset.mediaSubtypes.contains(.videoHighFrameRate) {
} else { self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaSlomo")
self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaVideo") } else if asset.mediaSubtypes.contains(.videoTimelapse) {
self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaTimelapse")
} else {
self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaVideo")
}
} }
self.durationNode.attributedText = NSAttributedString(string: stringForDuration(Int32(asset.duration)), font: Font.semibold(12.0), textColor: .white) self.durationNode.attributedText = NSAttributedString(string: stringForDuration(Int32(asset.duration)), font: Font.semibold(12.0), textColor: .white)
if self.typeIconNode.supernode == nil { if self.durationNode.supernode == nil {
self.addSubnode(self.gradientNode) self.addSubnode(self.gradientNode)
self.addSubnode(self.typeIconNode) self.addSubnode(self.typeIconNode)
self.addSubnode(self.durationNode) self.addSubnode(self.durationNode)
@ -588,7 +596,7 @@ final class MediaPickerGridItemNode: GridItemNode {
let backgroundSize = CGSize(width: self.bounds.width, height: floorToScreenPixels(self.bounds.height / 9.0 * 16.0)) let backgroundSize = CGSize(width: self.bounds.width, height: floorToScreenPixels(self.bounds.height / 9.0 * 16.0))
self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((self.bounds.height - backgroundSize.height) / 2.0)), size: backgroundSize) self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((self.bounds.height - backgroundSize.height) / 2.0)), size: backgroundSize)
self.imageNode.frame = self.bounds.insetBy(dx: -1.0 + UIScreenPixel, dy: -1.0 + UIScreenPixel) self.imageNode.frame = self.bounds
self.gradientNode.frame = CGRect(x: 0.0, y: self.bounds.height - 36.0, width: self.bounds.width, height: 36.0) self.gradientNode.frame = CGRect(x: 0.0, y: self.bounds.height - 36.0, width: self.bounds.width, height: 36.0)
self.typeIconNode.frame = CGRect(x: 0.0, y: self.bounds.height - 20.0, width: 19.0, height: 19.0) self.typeIconNode.frame = CGRect(x: 0.0, y: self.bounds.height - 20.0, width: 19.0, height: 19.0)
self.activateAreaNode.frame = self.bounds self.activateAreaNode.frame = self.bounds
@ -619,11 +627,11 @@ final class MediaPickerGridItemNode: GridItemNode {
func transitionView(snapshot: Bool) -> UIView { func transitionView(snapshot: Bool) -> UIView {
if snapshot { if snapshot {
let view = self.imageNode.view.snapshotContentTree(unhide: true, keepTransform: true)! let view = self.imageNode.layer.snapshotContentTreeAsView(unhide: true)!
view.frame = self.convert(self.bounds, to: nil) view.frame = self.convert(self.bounds, to: nil)
return view return view
} else { } else {
return self.imageNode.view return self.view
} }
} }

View File

@ -478,7 +478,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
let throttledContentOffsetSignal = self.fastScrollContentOffset.get() let throttledContentOffsetSignal = self.fastScrollContentOffset.get()
|> mapToThrottled { next -> Signal<CGPoint, NoError> in |> mapToThrottled { next -> Signal<CGPoint, NoError> in
return .single(next) |> then(.complete() |> delay(0.02, queue: Queue.concurrentDefaultQueue())) return .single(next) |> then(.complete() |> delay(0.05, queue: Queue.concurrentDefaultQueue()))
} }
self.fastScrollDisposable = (throttledContentOffsetSignal self.fastScrollDisposable = (throttledContentOffsetSignal
|> deliverOnMainQueue).start(next: { [weak self] contentOffset in |> deliverOnMainQueue).start(next: { [weak self] contentOffset in
@ -1266,7 +1266,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
itemHeight = floor(itemWidth * 1.227) itemHeight = floor(itemWidth * 1.227)
} }
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: bounds.size, insets: gridInsets, scrollIndicatorInsets: nil, preloadSize: itemHeight * 3.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemHeight), fillWidth: true, lineSpacing: itemSpacing, itemSpacing: itemSpacing), cutout: cameraRect), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { [weak self] _ in let preloadSize: CGFloat = itemHeight// * 3.0
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: bounds.size, insets: gridInsets, scrollIndicatorInsets: nil, preloadSize: preloadSize, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemHeight), fillWidth: true, lineSpacing: itemSpacing, itemSpacing: itemSpacing), cutout: cameraRect), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { [weak self] _ in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }

View File

@ -464,13 +464,16 @@ private final class StickerPackContainer: ASDisplayNode {
var menuItems: [ContextMenuItem] = [] var menuItems: [ContextMenuItem] = []
if let (info, _, _) = strongSelf.currentStickerPack, info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { if let (info, _, _) = strongSelf.currentStickerPack, info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
if strongSelf.sendSticker != nil { if strongSelf.sendSticker != nil {
var iconName: String
let actionTitle: String let actionTitle: String
if let title = strongSelf.controller?.actionTitle { if let title = strongSelf.controller?.actionTitle {
actionTitle = title actionTitle = title
iconName = "Chat/Context Menu/Add"
} else { } else {
actionTitle = strongSelf.presentationData.strings.StickerPack_Send actionTitle = strongSelf.presentationData.strings.StickerPack_Send
iconName = "Chat/Context Menu/Resend"
} }
menuItems.append(.action(ContextMenuActionItem(text: actionTitle, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in menuItems.append(.action(ContextMenuActionItem(text: actionTitle, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: iconName), color: theme.contextMenu.primaryColor) }, action: { _, f in
if let strongSelf = self, let peekController = strongSelf.peekController { if let strongSelf = self, let peekController = strongSelf.peekController {
if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode {
let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode.view, animationNode.bounds) let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode.view, animationNode.bounds)

View File

@ -71,12 +71,16 @@ swift_library(
"//submodules/Components/BlurredBackgroundComponent", "//submodules/Components/BlurredBackgroundComponent",
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
"//submodules/Components/BundleIconComponent:BundleIconComponent", "//submodules/Components/BundleIconComponent:BundleIconComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TooltipUI", "//submodules/TooltipUI",
"//submodules/TelegramUI/Components/MediaEditor", "//submodules/TelegramUI/Components/MediaEditor",
"//submodules/Components/MetalImageView", "//submodules/Components/MetalImageView",
"//submodules/TelegramUI/Components/CameraButtonComponent", "//submodules/TelegramUI/Components/CameraButtonComponent",
"//submodules/Utils/VolumeButtons", "//submodules/Utils/VolumeButtons",
"//submodules/TelegramNotices", "//submodules/TelegramNotices",
"//submodules/DeviceAccess",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -21,6 +21,7 @@ import BundleIconComponent
import CameraButtonComponent import CameraButtonComponent
import VolumeButtons import VolumeButtons
import TelegramNotices import TelegramNotices
import DeviceAccess
let videoRedColor = UIColor(rgb: 0xff3b30) let videoRedColor = UIColor(rgb: 0xff3b30)
@ -87,6 +88,8 @@ private final class CameraScreenComponent: CombinedComponent {
let context: AccountContext let context: AccountContext
let cameraState: CameraState let cameraState: CameraState
let cameraAuthorizationStatus: AccessType
let microphoneAuthorizationStatus: AccessType
let hasAppeared: Bool let hasAppeared: Bool
let isVisible: Bool let isVisible: Bool
let panelWidth: CGFloat let panelWidth: CGFloat
@ -101,6 +104,8 @@ private final class CameraScreenComponent: CombinedComponent {
init( init(
context: AccountContext, context: AccountContext,
cameraState: CameraState, cameraState: CameraState,
cameraAuthorizationStatus: AccessType,
microphoneAuthorizationStatus: AccessType,
hasAppeared: Bool, hasAppeared: Bool,
isVisible: Bool, isVisible: Bool,
panelWidth: CGFloat, panelWidth: CGFloat,
@ -114,6 +119,8 @@ private final class CameraScreenComponent: CombinedComponent {
) { ) {
self.context = context self.context = context
self.cameraState = cameraState self.cameraState = cameraState
self.cameraAuthorizationStatus = cameraAuthorizationStatus
self.microphoneAuthorizationStatus = microphoneAuthorizationStatus
self.hasAppeared = hasAppeared self.hasAppeared = hasAppeared
self.isVisible = isVisible self.isVisible = isVisible
self.panelWidth = panelWidth self.panelWidth = panelWidth
@ -133,6 +140,12 @@ private final class CameraScreenComponent: CombinedComponent {
if lhs.cameraState != rhs.cameraState { if lhs.cameraState != rhs.cameraState {
return false return false
} }
if lhs.cameraAuthorizationStatus != rhs.cameraAuthorizationStatus {
return false
}
if lhs.microphoneAuthorizationStatus != rhs.microphoneAuthorizationStatus {
return false
}
if lhs.hasAppeared != rhs.hasAppeared { if lhs.hasAppeared != rhs.hasAppeared {
return false return false
} }
@ -166,11 +179,7 @@ private final class CameraScreenComponent: CombinedComponent {
return image return image
} }
} }
private var cameraAuthorizationStatus: AVAuthorizationStatus = .notDetermined
private var microphoneAuthorizationStatus: AVAuthorizationStatus = .notDetermined
private var galleryAuthorizationStatus: PHAuthorizationStatus = .notDetermined
private let context: AccountContext private let context: AccountContext
private let present: (ViewController) -> Void private let present: (ViewController) -> Void
private let completion: ActionSlot<Signal<CameraScreen.Result, NoError>> private let completion: ActionSlot<Signal<CameraScreen.Result, NoError>>
@ -180,7 +189,7 @@ private final class CameraScreenComponent: CombinedComponent {
private let getController: () -> CameraScreen? private let getController: () -> CameraScreen?
private var resultDisposable = MetaDisposable() private var resultDisposable = MetaDisposable()
private var mediaAssetsContext: MediaAssetsContext? private var mediaAssetsContext: MediaAssetsContext?
fileprivate var lastGalleryAsset: PHAsset? fileprivate var lastGalleryAsset: PHAsset?
private var lastGalleryAssetsDisposable: Disposable? private var lastGalleryAssetsDisposable: Disposable?
@ -502,6 +511,7 @@ private final class CameraScreenComponent: CombinedComponent {
} }
static var body: Body { static var body: Body {
let placeholder = Child(PlaceholderComponent.self)
let cancelButton = Child(CameraButton.self) let cancelButton = Child(CameraButton.self)
let captureControls = Child(CaptureControlsComponent.self) let captureControls = Child(CaptureControlsComponent.self)
let zoomControl = Child(ZoomComponent.self) let zoomControl = Child(ZoomComponent.self)
@ -535,12 +545,51 @@ private final class CameraScreenComponent: CombinedComponent {
let panelWidth = min(component.panelWidth, 185.0) let panelWidth = min(component.panelWidth, 185.0)
var controlsBottomInset: CGFloat = 0.0 var controlsBottomInset: CGFloat = 0.0
let previewHeight = floorToScreenPixels(availableSize.width * 1.77778)
if !isTablet { if !isTablet {
let previewHeight = floorToScreenPixels(availableSize.width * 1.77778)
if availableSize.height < previewHeight + 30.0 { if availableSize.height < previewHeight + 30.0 {
controlsBottomInset = -48.0 controlsBottomInset = -48.0
} }
} }
let hasAllRequiredAccess: Bool
switch component.cameraAuthorizationStatus {
case .notDetermined:
hasAllRequiredAccess = true
case .allowed:
switch component.microphoneAuthorizationStatus {
case .notDetermined:
hasAllRequiredAccess = true
case .allowed:
hasAllRequiredAccess = true
default:
hasAllRequiredAccess = false
}
default:
hasAllRequiredAccess = false
}
if !hasAllRequiredAccess {
let accountContext = component.context
let placeholder = placeholder.update(
component: PlaceholderComponent(
context: component.context,
mode: .denied,
action: {
accountContext.sharedContext.applicationBindings.openSettings()
}
),
availableSize: CGSize(width: availableSize.width, height: previewHeight),
transition: context.transition
)
context.add(placeholder
.position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + previewHeight / 2.0))
.clipsToBounds(true)
.cornerRadius(11.0)
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
}
if case .holding = component.cameraState.recording { if case .holding = component.cameraState.recording {
@ -593,7 +642,8 @@ private final class CameraScreenComponent: CombinedComponent {
let captureControls = captureControls.update( let captureControls = captureControls.update(
component: CaptureControlsComponent( component: CaptureControlsComponent(
isTablet: isTablet, isTablet: isTablet,
hasAppeared: component.hasAppeared, hasAppeared: component.hasAppeared && hasAllRequiredAccess,
hasAccess: hasAllRequiredAccess,
shutterState: shutterState, shutterState: shutterState,
lastGalleryAsset: state.lastGalleryAsset, lastGalleryAsset: state.lastGalleryAsset,
tag: captureControlsTag, tag: captureControlsTag,
@ -739,51 +789,53 @@ private final class CameraScreenComponent: CombinedComponent {
) )
} }
let flashButton = flashButton.update( if hasAllRequiredAccess {
component: CameraButton( let flashButton = flashButton.update(
content: flashContentComponent,
action: { [weak state] in
if let state {
state.toggleFlashMode()
}
}
).tagged(flashButtonTag),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(flashButton
.position(CGPoint(x: isTablet ? availableSize.width - smallPanelWidth / 2.0 : availableSize.width - topControlInset - flashButton.size.width / 2.0 - 5.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + flashButton.size.height / 2.0))
.appear(.default(scale: true))
.disappear(.default(scale: true))
)
if !isTablet && Camera.isDualCameraSupported {
let dualButton = dualButton.update(
component: CameraButton( component: CameraButton(
content: AnyComponentWithIdentity( content: flashContentComponent,
id: "dual",
component: AnyComponent(
DualIconComponent(isSelected: component.cameraState.isDualCameraEnabled)
)
),
action: { [weak state] in action: { [weak state] in
if let state { if let state {
state.toggleDualCamera() state.toggleFlashMode()
} }
} }
).tagged(dualButtonTag), ).tagged(flashButtonTag),
availableSize: CGSize(width: 40.0, height: 40.0), availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate transition: .immediate
) )
context.add(dualButton context.add(flashButton
.position(CGPoint(x: availableSize.width - topControlInset - flashButton.size.width / 2.0 - 58.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + dualButton.size.height / 2.0 + 2.0)) .position(CGPoint(x: isTablet ? availableSize.width - smallPanelWidth / 2.0 : availableSize.width - topControlInset - flashButton.size.width / 2.0 - 5.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + flashButton.size.height / 2.0))
.appear(.default(scale: true)) .appear(.default(scale: true))
.disappear(.default(scale: true)) .disappear(.default(scale: true))
) )
if !isTablet && Camera.isDualCameraSupported {
let dualButton = dualButton.update(
component: CameraButton(
content: AnyComponentWithIdentity(
id: "dual",
component: AnyComponent(
DualIconComponent(isSelected: component.cameraState.isDualCameraEnabled)
)
),
action: { [weak state] in
if let state {
state.toggleDualCamera()
}
}
).tagged(dualButtonTag),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(dualButton
.position(CGPoint(x: availableSize.width - topControlInset - flashButton.size.width / 2.0 - 58.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + dualButton.size.height / 2.0 + 2.0))
.appear(.default(scale: true))
.disappear(.default(scale: true))
)
}
} }
} }
if isTablet { if isTablet && hasAllRequiredAccess {
let flipButton = flipButton.update( let flipButton = flipButton.update(
component: CameraButton( component: CameraButton(
content: AnyComponentWithIdentity( content: AnyComponentWithIdentity(
@ -807,6 +859,8 @@ private final class CameraScreenComponent: CombinedComponent {
) )
context.add(flipButton context.add(flipButton
.position(CGPoint(x: smallPanelWidth / 2.0, y: availableSize.height / 2.0)) .position(CGPoint(x: smallPanelWidth / 2.0, y: availableSize.height / 2.0))
.appear(.default(scale: true))
.disappear(.default(scale: true))
) )
} }
@ -885,7 +939,7 @@ private final class CameraScreenComponent: CombinedComponent {
} }
} }
if case .none = component.cameraState.recording, !state.isTransitioning { if case .none = component.cameraState.recording, !state.isTransitioning && hasAllRequiredAccess {
let availableModeControlSize: CGSize let availableModeControlSize: CGSize
if isTablet { if isTablet {
availableModeControlSize = CGSize(width: panelWidth, height: 120.0) availableModeControlSize = CGSize(width: panelWidth, height: 120.0)
@ -1141,6 +1195,11 @@ public class CameraScreen: ViewController {
} }
} }
} }
private var cameraAuthorizationStatus: AccessType = .notDetermined
private var microphoneAuthorizationStatus: AccessType = .notDetermined
private var galleryAuthorizationStatus: AccessType = .notDetermined
private var authorizationStatusDisposables = DisposableSet()
init(controller: CameraScreen) { init(controller: CameraScreen) {
self.controller = controller self.controller = controller
@ -1312,12 +1371,33 @@ public class CameraScreen: ViewController {
} }
self.idleTimerExtensionDisposable.set(self.context.sharedContext.applicationBindings.pushIdleTimerExtension()) self.idleTimerExtensionDisposable.set(self.context.sharedContext.applicationBindings.pushIdleTimerExtension())
self.authorizationStatusDisposables.add((DeviceAccess.authorizationStatus(subject: .camera(.video))
|> deliverOnMainQueue).start(next: { [weak self] status in
if let self {
self.cameraAuthorizationStatus = status
self.requestUpdateLayout(hasAppeared: self.hasAppeared, transition: .easeInOut(duration: 0.2))
self.maybeSetupCamera()
}
}))
self.authorizationStatusDisposables.add((DeviceAccess.authorizationStatus(subject: .microphone(.video))
|> deliverOnMainQueue).start(next: { [weak self] status in
if let self {
self.microphoneAuthorizationStatus = status
self.requestUpdateLayout(hasAppeared: self.hasAppeared, transition: .easeInOut(duration: 0.2))
self.maybeSetupCamera()
}
}))
} }
deinit { deinit {
self.cameraStateDisposable?.dispose() self.cameraStateDisposable?.dispose()
self.changingPositionDisposable?.dispose() self.changingPositionDisposable?.dispose()
self.idleTimerExtensionDisposable.dispose() self.idleTimerExtensionDisposable.dispose()
self.authorizationStatusDisposables.dispose()
} }
private var pipPanGestureRecognizer: UIPanGestureRecognizer? private var pipPanGestureRecognizer: UIPanGestureRecognizer?
@ -1346,11 +1426,23 @@ public class CameraScreen: ViewController {
pipPanGestureRecognizer.delegate = self pipPanGestureRecognizer.delegate = self
self.previewContainerView.addGestureRecognizer(pipPanGestureRecognizer) self.previewContainerView.addGestureRecognizer(pipPanGestureRecognizer)
self.pipPanGestureRecognizer = pipPanGestureRecognizer self.pipPanGestureRecognizer = pipPanGestureRecognizer
self.setupCamera()
} }
func setupCamera() { private func maybeSetupCamera() {
if case .allowed = self.cameraAuthorizationStatus, case .allowed = self.microphoneAuthorizationStatus {
self.setupCamera()
}
}
private func requestDeviceAccess() {
DeviceAccess.authorizeAccess(to: .camera(.video), { granted in
if granted {
DeviceAccess.authorizeAccess(to: .microphone(.video))
}
})
}
private func setupCamera() {
guard self.camera == nil else { guard self.camera == nil else {
return return
} }
@ -1461,6 +1553,10 @@ public class CameraScreen: ViewController {
camera.startCapture() camera.startCapture()
self.camera = camera self.camera = camera
if self.hasAppeared {
self.maybePresentTooltips()
}
} }
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
@ -1581,7 +1677,7 @@ public class CameraScreen: ViewController {
CATransaction.begin() CATransaction.begin()
CATransaction.setDisableActions(true) CATransaction.setDisableActions(true)
self.requestUpdateLayout(hasAppeared: false, transition: .immediate) self.requestUpdateLayout(hasAppeared: self.hasAppeared, transition: .immediate)
CATransaction.commit() CATransaction.commit()
self.additionalPreviewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.additionalPreviewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
@ -1866,9 +1962,12 @@ public class CameraScreen: ViewController {
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY + 3.0), size: CGSize()) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY + 3.0), size: CGSize())
let accountManager = self.context.sharedContext.accountManager let accountManager = self.context.sharedContext.accountManager
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: "Take photos or videos to share with all\nyour contacts or close friends at once."), textAlignment: .center, location: .point(location, .bottom), displayDuration: .custom(4.5), inset: 16.0, shouldDismissOnTouch: { point, containerFrame in let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: "Take photos or videos to share with all\nyour contacts or close friends at once."), textAlignment: .center, location: .point(location, .bottom), displayDuration: .custom(4.5), inset: 16.0, shouldDismissOnTouch: { [weak self] point, containerFrame in
if containerFrame.contains(point) { if containerFrame.contains(point) {
let _ = ApplicationSpecificNotice.incrementStoriesCameraTip(accountManager: accountManager).start() let _ = ApplicationSpecificNotice.incrementStoriesCameraTip(accountManager: accountManager).start()
Queue.mainQueue().justDispatch {
self?.maybePresentTooltips()
}
return .dismiss(consume: true) return .dismiss(consume: true)
} }
return .ignore return .ignore
@ -2006,7 +2105,11 @@ public class CameraScreen: ViewController {
self.hasAppeared = hasAppeared self.hasAppeared = hasAppeared
transition = transition.withUserData(CameraScreenTransition.finishedAnimateIn) transition = transition.withUserData(CameraScreenTransition.finishedAnimateIn)
self.maybePresentTooltips() if self.camera != nil {
self.maybePresentTooltips()
} else if case .notDetermined = self.cameraAuthorizationStatus {
self.requestDeviceAccess()
}
} }
let componentSize = self.componentHost.update( let componentSize = self.componentHost.update(
@ -2015,6 +2118,8 @@ public class CameraScreen: ViewController {
CameraScreenComponent( CameraScreenComponent(
context: self.context, context: self.context,
cameraState: self.cameraState, cameraState: self.cameraState,
cameraAuthorizationStatus: self.cameraAuthorizationStatus,
microphoneAuthorizationStatus: self.microphoneAuthorizationStatus,
hasAppeared: self.hasAppeared, hasAppeared: self.hasAppeared,
isVisible: self.cameraIsActive && !self.hasGallery, isVisible: self.cameraIsActive && !self.hasGallery,
panelWidth: panelWidth, panelWidth: panelWidth,

View File

@ -446,6 +446,7 @@ final class CaptureControlsComponent: Component {
let isTablet: Bool let isTablet: Bool
let hasAppeared: Bool let hasAppeared: Bool
let hasAccess: Bool
let shutterState: ShutterButtonState let shutterState: ShutterButtonState
let lastGalleryAsset: PHAsset? let lastGalleryAsset: PHAsset?
let tag: AnyObject? let tag: AnyObject?
@ -463,6 +464,7 @@ final class CaptureControlsComponent: Component {
init( init(
isTablet: Bool, isTablet: Bool,
hasAppeared: Bool, hasAppeared: Bool,
hasAccess: Bool,
shutterState: ShutterButtonState, shutterState: ShutterButtonState,
lastGalleryAsset: PHAsset?, lastGalleryAsset: PHAsset?,
tag: AnyObject?, tag: AnyObject?,
@ -479,6 +481,7 @@ final class CaptureControlsComponent: Component {
) { ) {
self.isTablet = isTablet self.isTablet = isTablet
self.hasAppeared = hasAppeared self.hasAppeared = hasAppeared
self.hasAccess = hasAccess
self.shutterState = shutterState self.shutterState = shutterState
self.lastGalleryAsset = lastGalleryAsset self.lastGalleryAsset = lastGalleryAsset
self.tag = tag self.tag = tag
@ -501,6 +504,9 @@ final class CaptureControlsComponent: Component {
if lhs.hasAppeared != rhs.hasAppeared { if lhs.hasAppeared != rhs.hasAppeared {
return false return false
} }
if lhs.hasAccess != rhs.hasAccess {
return false
}
if lhs.shutterState != rhs.shutterState { if lhs.shutterState != rhs.shutterState {
return false return false
} }
@ -944,7 +950,7 @@ final class CaptureControlsComponent: Component {
transition.setAlpha(view: galleryButtonView, alpha: isRecording || isTransitioning ? 0.0 : 1.0) transition.setAlpha(view: galleryButtonView, alpha: isRecording || isTransitioning ? 0.0 : 1.0)
} }
if !component.isTablet { if !component.isTablet && component.hasAccess {
let flipButtonOriginX = availableSize.width - 48.0 - buttonSideInset let flipButtonOriginX = availableSize.width - 48.0 - buttonSideInset
let flipButtonMaskFrame: CGRect = CGRect(origin: CGPoint(x: availableSize.width / 2.0 - (flipButtonOriginX + 22.0) + 6.0 + self.shutterOffsetX, y: 8.0), size: CGSize(width: 32.0, height: 32.0)) let flipButtonMaskFrame: CGRect = CGRect(origin: CGPoint(x: availableSize.width / 2.0 - (flipButtonOriginX + 22.0) + 6.0 + self.shutterOffsetX, y: 8.0), size: CGSize(width: 32.0, height: 32.0))
@ -1153,10 +1159,13 @@ final class CaptureControlsComponent: Component {
self.addSubview(shutterButtonView) self.addSubview(shutterButtonView)
} }
let alpha: CGFloat = component.hasAccess ? 1.0 : 0.3
transition.setBounds(view: shutterButtonView, bounds: CGRect(origin: .zero, size: shutterButtonFrame.size)) transition.setBounds(view: shutterButtonView, bounds: CGRect(origin: .zero, size: shutterButtonFrame.size))
transition.setPosition(view: shutterButtonView, position: shutterButtonFrame.center) transition.setPosition(view: shutterButtonView, position: shutterButtonFrame.center)
transition.setScale(view: shutterButtonView, scale: isTransitioning ? 0.01 : 1.0) transition.setScale(view: shutterButtonView, scale: isTransitioning ? 0.01 : 1.0)
transition.setAlpha(view: shutterButtonView, alpha: isTransitioning ? 0.0 : 1.0) transition.setAlpha(view: shutterButtonView, alpha: isTransitioning ? 0.0 : alpha)
shutterButtonView.isUserInteractionEnabled = component.hasAccess
} }
if let buttonView = self.flipButtonView.view as? CameraButton.View, let contentView = buttonView.contentView.componentView as? FlipButtonContentComponent.View { if let buttonView = self.flipButtonView.view as? CameraButton.View, let contentView = buttonView.contentView.componentView as? FlipButtonContentComponent.View {

View File

@ -0,0 +1,224 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import AccountContext
import BundleIconComponent
import MultilineTextComponent
import ButtonComponent
import LottieComponent
final class PlaceholderComponent: Component {
typealias EnvironmentType = Empty
enum Mode {
case request
case denied
}
let context: AccountContext
let mode: Mode
let action: () -> Void
init(
context: AccountContext,
mode: Mode,
action: @escaping () -> Void
) {
self.context = context
self.mode = mode
self.action = action
}
static func ==(lhs: PlaceholderComponent, rhs: PlaceholderComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.mode != rhs.mode {
return false
}
return true
}
public final class View: UIView {
private let animation = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let text = ComponentView<Empty>()
private let button = ComponentView<Empty>()
private var component: PlaceholderComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor(rgb: 0x1c1c1e)
// if #available(iOS 13.0, *) {
// self.layer.cornerCurve = .continuous
// }
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: PlaceholderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.state = state
let sideInset: CGFloat = 36.0
let animationHeight: CGFloat = 120.0
let title: String = "Allow Telegram to access your camera and microphone"
let text: String = "This lets you share photos and record videos."
let buttonTitle: String = "Open Settings"
let animationSize = self.animation.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "Photos")
)),
environment: {},
containerSize: CGSize(width: animationHeight, height: animationHeight)
)
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: title, font: Font.semibold(17.0), textColor: UIColor.white)),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 3.0, height: availableSize.height)
)
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: text, font: Font.regular(15.0), textColor: UIColor(rgb: 0x98989f))),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
)
let buttonSize = self.button.update(
transition: .immediate,
component: AnyComponent(
ButtonComponent(
background: ButtonComponent.Background(
color: UIColor(rgb: 0x007aff),
foreground: .white,
pressedColor: UIColor(rgb: 0x007aff, alpha: 0.55)
),
content: AnyComponentWithIdentity(
id: buttonTitle,
component: AnyComponent(ButtonTextContentComponent(
text: buttonTitle,
badge: 0,
textColor: .white,
badgeBackground: .clear,
badgeForeground: .clear
))
),
isEnabled: true,
displaysProgress: false,
action: { [weak self] in
if let self {
self.component?.action()
}
}
)
),
environment: {},
containerSize: CGSize(width: 240.0, height: 50.0)
)
let titleSpacing: CGFloat = 12.0
let textSpacing: CGFloat = 14.0
let buttonSpacing: CGFloat = 18.0
let totalHeight = animationSize.height + titleSpacing + titleSize.height + textSpacing + textSize.height + buttonSpacing + buttonSize.height
var originY = floorToScreenPixels((availableSize.height - totalHeight) / 2.0)
let animationFrame = CGRect(
origin: CGPoint(
x: floorToScreenPixels((availableSize.width - animationSize.width) / 2.0),
y: originY
),
size: animationSize
)
if let view = self.animation.view as? LottieComponent.View {
if view.superview == nil {
self.addSubview(view)
Queue.mainQueue().justDispatch {
view.playOnce()
}
}
view.frame = animationFrame
}
originY += animationSize.height + titleSpacing
let titleFrame = CGRect(
origin: CGPoint(
x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0),
y: originY
),
size: titleSize
)
if let view = self.title.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = titleFrame
}
originY += titleSize.height + textSpacing
let textFrame = CGRect(
origin: CGPoint(
x: floorToScreenPixels((availableSize.width - textSize.width) / 2.0),
y: originY
),
size: textSize
)
if let view = self.text.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = textFrame
}
originY += textSize.height + buttonSpacing
let buttonFrame = CGRect(
origin: CGPoint(
x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0),
y: originY
),
size: buttonSize
)
if let view = self.button.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = buttonFrame
}
return availableSize
}
}
func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -221,7 +221,10 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode {
if let strongSelf = self { if let strongSelf = self {
strongSelf.view.window?.endEditing(true) strongSelf.view.window?.endEditing(true)
let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash)
let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.interaction.getNavigationController(), sendSticker: { [weak self] fileReference, sourceNode, sourceRect in
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: theme)
let controller = StickerPackScreen(context: strongSelf.context, updatedPresentationData: (presentationData, .single(presentationData)), mainStickerPack: packReference, stickerPacks: [packReference], actionTitle: stickerActionTitle, parentNavigationController: strongSelf.interaction.getNavigationController(), sendSticker: { [weak self] fileReference, sourceNode, sourceRect in
if let strongSelf = self { if let strongSelf = self {
return strongSelf.interaction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, []) return strongSelf.interaction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, [])
} else { } else {

View File

@ -14,8 +14,13 @@ private func fullEntityMediaPath(_ path: String) -> String {
public final class DrawingStickerEntity: DrawingEntity, Codable { public final class DrawingStickerEntity: DrawingEntity, Codable {
public enum Content: Equatable { public enum Content: Equatable {
public enum ImageType: Equatable {
case sticker
case rectangle
case dualPhoto
}
case file(TelegramMediaFile) case file(TelegramMediaFile)
case image(UIImage, Bool) case image(UIImage, ImageType)
case video(String, UIImage?, Bool) case video(String, UIImage?, Bool)
case dualVideoReference case dualVideoReference
@ -27,9 +32,9 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
} else { } else {
return false return false
} }
case let .image(lhsImage, lhsIsRectangle): case let .image(lhsImage, lhsImageType):
if case let .image(rhsImage, rhsIsRectangle) = rhs { if case let .image(rhsImage, rhsImageType) = rhs {
return lhsImage === rhsImage && lhsIsRectangle == rhsIsRectangle return lhsImage === rhsImage && lhsImageType == rhsImageType
} else { } else {
return false return false
} }
@ -56,6 +61,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
case videoImagePath case videoImagePath
case videoMirrored case videoMirrored
case isRectangle case isRectangle
case isDualPhoto
case dualVideo case dualVideo
case referenceDrawingSize case referenceDrawingSize
case position case position
@ -121,8 +127,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
public var isRectangle: Bool { public var isRectangle: Bool {
switch self.content { switch self.content {
case let .image(_, isRectangle): case let .image(_, imageType):
return isRectangle return imageType == .rectangle
default: default:
return false return false
} }
@ -157,7 +163,16 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
self.content = .file(file) self.content = .file(file)
} else if let imagePath = try container.decodeIfPresent(String.self, forKey: .imagePath), let image = UIImage(contentsOfFile: fullEntityMediaPath(imagePath)) { } else if let imagePath = try container.decodeIfPresent(String.self, forKey: .imagePath), let image = UIImage(contentsOfFile: fullEntityMediaPath(imagePath)) {
let isRectangle = try container.decodeIfPresent(Bool.self, forKey: .isRectangle) ?? false let isRectangle = try container.decodeIfPresent(Bool.self, forKey: .isRectangle) ?? false
self.content = .image(image, isRectangle) let isDualPhoto = try container.decodeIfPresent(Bool.self, forKey: .isDualPhoto) ?? false
let imageType: Content.ImageType
if isDualPhoto {
imageType = .dualPhoto
} else if isRectangle {
imageType = .rectangle
} else {
imageType = .sticker
}
self.content = .image(image, imageType)
} else if let videoPath = try container.decodeIfPresent(String.self, forKey: .videoPath) { } else if let videoPath = try container.decodeIfPresent(String.self, forKey: .videoPath) {
var imageValue: UIImage? var imageValue: UIImage?
if let imagePath = try container.decodeIfPresent(String.self, forKey: .videoImagePath), let image = UIImage(contentsOfFile: fullEntityMediaPath(imagePath)) { if let imagePath = try container.decodeIfPresent(String.self, forKey: .videoImagePath), let image = UIImage(contentsOfFile: fullEntityMediaPath(imagePath)) {
@ -182,7 +197,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
switch self.content { switch self.content {
case let .file(file): case let .file(file):
try container.encode(file, forKey: .file) try container.encode(file, forKey: .file)
case let .image(image, isRectangle): case let .image(image, imageType):
let imagePath = "\(self.uuid).png" let imagePath = "\(self.uuid).png"
let fullImagePath = fullEntityMediaPath(imagePath) let fullImagePath = fullEntityMediaPath(imagePath)
if let imageData = image.pngData() { if let imageData = image.pngData() {
@ -190,7 +205,14 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
try? imageData.write(to: URL(fileURLWithPath: fullImagePath)) try? imageData.write(to: URL(fileURLWithPath: fullImagePath))
try container.encodeIfPresent(imagePath, forKey: .imagePath) try container.encodeIfPresent(imagePath, forKey: .imagePath)
} }
try container.encode(isRectangle, forKey: .isRectangle) switch imageType {
case .dualPhoto:
try container.encode(true, forKey: .isDualPhoto)
case .rectangle:
try container.encode(true, forKey: .isRectangle)
default:
break
}
case let .video(path, image, videoMirrored): case let .video(path, image, videoMirrored):
try container.encode(path, forKey: .videoPath) try container.encode(path, forKey: .videoPath)
let imagePath = "\(self.uuid).jpg" let imagePath = "\(self.uuid).jpg"

View File

@ -481,7 +481,8 @@ public final class MediaEditor {
if player == nil { if player == nil {
self.updateRenderChain() self.updateRenderChain()
self.maybeGeneratePersonSegmentation(image) let _ = image
// self.maybeGeneratePersonSegmentation(image)
} }
if let player { if let player {

View File

@ -1167,13 +1167,13 @@ final class MediaEditorScreenComponent: Component {
switch data { switch data {
case let .sticker(image, _): case let .sticker(image, _):
if max(image.size.width, image.size.height) > 1.0 { if max(image.size.width, image.size.height) > 1.0 {
let entity = DrawingStickerEntity(content: .image(image, false)) let entity = DrawingStickerEntity(content: .image(image, .sticker))
controller.node.interaction?.insertEntity(entity, scale: 1.0) controller.node.interaction?.insertEntity(entity, scale: 1.0)
self.deactivateInput() self.deactivateInput()
} }
case let .images(images): case let .images(images):
if images.count == 1, let image = images.first, max(image.size.width, image.size.height) > 1.0 { if images.count == 1, let image = images.first, max(image.size.width, image.size.height) > 1.0 {
let entity = DrawingStickerEntity(content: .image(image, true)) let entity = DrawingStickerEntity(content: .image(image, .rectangle))
controller.node.interaction?.insertEntity(entity, scale: 2.5) controller.node.interaction?.insertEntity(entity, scale: 2.5)
self.deactivateInput() self.deactivateInput()
} }
@ -1925,8 +1925,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
if let cgImage = additionalImage.cgImage { if let cgImage = additionalImage.cgImage {
context.draw(cgImage, in: CGRect(origin: CGPoint(x: (size.width - additionalImage.size.width) / 2.0, y: (size.height - additionalImage.size.height) / 2.0), size: additionalImage.size)) context.draw(cgImage, in: CGRect(origin: CGPoint(x: (size.width - additionalImage.size.width) / 2.0, y: (size.height - additionalImage.size.height) / 2.0), size: additionalImage.size))
} }
}) }, scale: 1.0)
let imageEntity = DrawingStickerEntity(content: .image(image ?? additionalImage, false)) let imageEntity = DrawingStickerEntity(content: .image(image ?? additionalImage, .dualPhoto))
imageEntity.referenceDrawingSize = storyDimensions imageEntity.referenceDrawingSize = storyDimensions
imageEntity.scale = 1.625 imageEntity.scale = 1.625
imageEntity.position = position.getPosition(storyDimensions) imageEntity.position = position.getPosition(storyDimensions)
@ -2730,7 +2730,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { [weak self] image, _ in PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { [weak self] image, _ in
if let self, let image { if let self, let image {
Queue.mainQueue().async { Queue.mainQueue().async {
self.interaction?.insertEntity(DrawingStickerEntity(content: .image(image, true)), scale: 2.5) self.interaction?.insertEntity(DrawingStickerEntity(content: .image(image, .rectangle)), scale: 2.5)
} }
} }
} }
@ -3862,7 +3862,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
if let self { if let self {
makeEditorImageComposition(context: self.node.ciContext, account: self.context.account, inputImage: image ?? UIImage(), dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { [weak self] coverImage in makeEditorImageComposition(context: self.node.ciContext, account: self.context.account, inputImage: image ?? UIImage(), dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { [weak self] coverImage in
if let self { if let self {
Logger.shared.log("Media Editor", "completed with video \(videoResult)") Logger.shared.log("MediaEditor", "Completed with video \(videoResult)")
self.completion(randomId, .video(video: videoResult, coverImage: coverImage, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions), caption, self.state.privacy, stickers, { [weak self] finished in self.completion(randomId, .video(video: videoResult, coverImage: coverImage, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions), caption, self.state.privacy, stickers, { [weak self] finished in
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
self?.dismiss() self?.dismiss()
@ -3885,7 +3885,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
makeEditorImageComposition(context: self.node.ciContext, account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { [weak self] resultImage in makeEditorImageComposition(context: self.node.ciContext, account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { [weak self] resultImage in
if let self, let resultImage { if let self, let resultImage {
Logger.shared.log("Media Editor", "completed with image \(resultImage)") Logger.shared.log("MediaEditor", "Completed with image \(resultImage)")
self.completion(randomId, .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), caption, self.state.privacy, stickers, { [weak self] finished in self.completion(randomId, .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), caption, self.state.privacy, stickers, { [weak self] finished in
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
self?.dismiss() self?.dismiss()
@ -4110,7 +4110,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
} }
let images = imageItems as! [UIImage] let images = imageItems as! [UIImage]
if images.count == 1, let image = images.first, max(image.size.width, image.size.height) > 1.0 { if images.count == 1, let image = images.first, max(image.size.width, image.size.height) > 1.0 {
self.node.interaction?.insertEntity(DrawingStickerEntity(content: .image(image, false)), scale: 2.5) self.node.interaction?.insertEntity(DrawingStickerEntity(content: .image(image, .sticker)), scale: 2.5)
} }
} }
} }

View File

@ -978,7 +978,7 @@ final class ShareWithPeersScreenComponent: Component {
} }
let fadeTransition = Transition.easeInOut(duration: 0.25) let fadeTransition = Transition.easeInOut(duration: 0.25)
if let searchStateContext = self.searchStateContext, case let .search(query) = searchStateContext.subject, let value = searchStateContext.stateValue, value.peers.isEmpty { if let searchStateContext = self.searchStateContext, case let .search(query, _) = searchStateContext.subject, let value = searchStateContext.stateValue, value.peers.isEmpty {
let sideInset: CGFloat = 44.0 let sideInset: CGFloat = 44.0
let emptyAnimationHeight = 148.0 let emptyAnimationHeight = 148.0
let topInset: CGFloat = topOffset + itemLayout.containerInset + 40.0 let topInset: CGFloat = topOffset + itemLayout.containerInset + 40.0
@ -1280,10 +1280,14 @@ final class ShareWithPeersScreenComponent: Component {
) )
if !self.navigationTextFieldState.text.isEmpty { if !self.navigationTextFieldState.text.isEmpty {
if let searchStateContext = self.searchStateContext, searchStateContext.subject == .search(self.navigationTextFieldState.text) { var onlyContacts = false
if component.initialPrivacy.base == .closeFriends || component.initialPrivacy.base == .contacts {
onlyContacts = true
}
if let searchStateContext = self.searchStateContext, searchStateContext.subject == .search(query: self.navigationTextFieldState.text, onlyContacts: onlyContacts) {
} else { } else {
self.searchStateDisposable?.dispose() self.searchStateDisposable?.dispose()
let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: .search(self.navigationTextFieldState.text)) let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: .search(query: self.navigationTextFieldState.text, onlyContacts: onlyContacts))
var applyState = false var applyState = false
self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
guard let self else { guard let self else {
@ -1335,7 +1339,7 @@ final class ShareWithPeersScreenComponent: Component {
sideInset: sideInset, sideInset: sideInset,
title: "Name", title: "Name",
peer: nil, peer: nil,
subtitle: "sub", subtitle: self.searchStateContext != nil ? "" : "sub",
subtitleAccessory: .none, subtitleAccessory: .none,
presence: nil, presence: nil,
selectionState: .editing(isSelected: false, isTinted: false), selectionState: .editing(isSelected: false, isTinted: false),
@ -1753,7 +1757,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
case stories(editing: Bool) case stories(editing: Bool)
case chats case chats
case contacts(EngineStoryPrivacy.Base) case contacts(EngineStoryPrivacy.Base)
case search(String) case search(query: String, onlyContacts: Bool)
} }
fileprivate var stateValue: State? fileprivate var stateValue: State?
@ -1889,7 +1893,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
self.readySubject.set(true) self.readySubject.set(true)
}) })
case let .search(query): case let .search(query, _):
self.stateDisposable = (context.engine.contacts.searchLocalPeers(query: query) self.stateDisposable = (context.engine.contacts.searchLocalPeers(query: query)
|> deliverOnMainQueue).start(next: { [weak self] peers in |> deliverOnMainQueue).start(next: { [weak self] peers in
guard let self else { guard let self else {