diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index d52c85c547..c3074e7c4b 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -2062,7 +2062,7 @@ "StickerPack.Share" = "Share"; "StickerPack.Send" = "Send Sticker"; -"StickerPack.Select" = "Select Sticker"; +"StickerPack.AddSticker" = "Add Sticker"; "StickerPack.RemoveStickerCount_1" = "Remove 1 Sticker"; "StickerPack.RemoveStickerCount_2" = "Remove 2 Stickers"; diff --git a/submodules/Camera/Sources/CameraMetrics.swift b/submodules/Camera/Sources/CameraMetrics.swift index 88b2ee4c9a..f4bc7f8a7c 100644 --- a/submodules/Camera/Sources/CameraMetrics.swift +++ b/submodules/Camera/Sources/CameraMetrics.swift @@ -57,6 +57,37 @@ enum DeviceModel: CaseIterable, Equatable { .iPodTouch5, .iPodTouch6, .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, .iPhone14Plus, .iPhone14Pro, @@ -72,9 +103,40 @@ enum DeviceModel: CaseIterable, Equatable { case iPodTouch6 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 iPhoneXS + case iPhoneXSMax + case iPhoneXR + case iPhone11 + case iPhone11Pro + case iPhone11ProMax + + case iPhoneSE2ndGen + case iPhone12 case iPhone12Mini case iPhone12Pro @@ -85,6 +147,8 @@ enum DeviceModel: CaseIterable, Equatable { case iPhone13Pro case iPhone13ProMax + case iPhoneSE3rdGen + case iPhone14 case iPhone14Plus case iPhone14Pro @@ -108,10 +172,56 @@ enum DeviceModel: CaseIterable, Equatable { return ["iPod7,1"] case .iPodTouch7: 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: - return ["iPhone11,2"] + return ["iPhone10,3", "iPhone10,6"] case .iPhoneXS: + return ["iPhone11,2"] + case .iPhoneXSMax: 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: return ["iPhone13,2"] case .iPhone12Mini: @@ -128,6 +238,8 @@ enum DeviceModel: CaseIterable, Equatable { return ["iPhone14,2"] case .iPhone13ProMax: return ["iPhone14,3"] + case .iPhoneSE3rdGen: + return ["iPhone14,6"] case .iPhone14: return ["iPhone14,7"] case .iPhone14Plus: @@ -157,10 +269,56 @@ enum DeviceModel: CaseIterable, Equatable { return "iPod touch 6G" case .iPodTouch7: 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: return "iPhone X" case .iPhoneXS: 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: return "iPhone 12" case .iPhone12Mini: @@ -177,6 +335,8 @@ enum DeviceModel: CaseIterable, Equatable { return "iPhone 13 Pro" case .iPhone13ProMax: return "iPhone 13 Pro Max" + case .iPhoneSE3rdGen: + return "iPhone SE (3rd gen)" case .iPhone14: return "iPhone 14" case .iPhone14Plus: diff --git a/submodules/DeviceAccess/Sources/DeviceAccess.swift b/submodules/DeviceAccess/Sources/DeviceAccess.swift index eeab79c9d2..6fba228c80 100644 --- a/submodules/DeviceAccess/Sources/DeviceAccess.swift +++ b/submodules/DeviceAccess/Sources/DeviceAccess.swift @@ -82,6 +82,16 @@ public final class DeviceAccess { return self.locationPromise.get() } + private static let cameraPromise = Promise(nil) + static var camera: Signal { + return self.cameraPromise.get() + } + + private static let microphonePromise = Promise(nil) + static var microphone: Signal { + return self.microphonePromise.get() + } + public static func isMicrophoneAccessAuthorized() -> Bool? { 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 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 in + if let authorized = authorized { + return .single(authorized ? .allowed : .denied) + } else { + return .complete() + } + } + ) default: 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 { case let .camera(cameraSubject): let status = AVCaptureDevice.authorizationStatus(for: .video) @@ -262,6 +332,7 @@ public final class DeviceAccess { AVCaptureDevice.requestAccess(for: AVMediaType.video) { response in Queue.mainQueue().async { completion(response) + self.cameraPromise.set(.single(response)) if !response, let presentationData = presentationData { let text: String switch cameraSubject { @@ -331,6 +402,7 @@ public final class DeviceAccess { displayNotificationFromBackground(text) } } + self.microphonePromise.set(.single(granted)) } }) } diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 487d3b6280..5eeb45a852 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -694,6 +694,8 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { if let selectedEntityView = self.selectedEntityView, let selectionView = selectedEntityView.selectionView { if !self.hasBin { selectionView.handlePan(gestureRecognizer) + } else if let stickerEntity = selectedEntityView.entity as? DrawingStickerEntity, case .dualVideoReference = stickerEntity.content { + selectionView.handlePan(gestureRecognizer) } else { var isTrappedInBin = false let scale = 100.0 / selectedEntityView.bounds.size.width diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 163617b6ec..bebb78e69b 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -2930,7 +2930,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U } let images = imageItems as! [UIImage] 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) } } diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/DrawingUI/Sources/StickerPickerScreen.swift index a616541fe3..80d03ef30c 100644 --- a/submodules/DrawingUI/Sources/StickerPickerScreen.swift +++ b/submodules/DrawingUI/Sources/StickerPickerScreen.swift @@ -289,7 +289,7 @@ private final class StickerSelectionComponent: Component { interaction: interaction, inputNodeInteraction: inputNodeInteraction, mode: mappedMode, - stickerActionTitle: presentationData.strings.StickerPack_Select, + stickerActionTitle: presentationData.strings.StickerPack_AddSticker, trendingGifsPromise: trendingGifsPromise, cancel: { }, @@ -585,7 +585,7 @@ public class StickerPickerScreen: ViewController { CTLineDraw(line, context) 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) } diff --git a/submodules/MediaPickerUI/Sources/FetchAssets.swift b/submodules/MediaPickerUI/Sources/FetchAssets.swift index 7b236c40a7..d9610f1272 100644 --- a/submodules/MediaPickerUI/Sources/FetchAssets.swift +++ b/submodules/MediaPickerUI/Sources/FetchAssets.swift @@ -261,6 +261,7 @@ func assetImage(asset: PHAsset, targetSize: CGSize, exact: Bool, deliveryMode: P if let info = info { if let cancelled = info[PHImageCancelledKey] as? Bool, cancelled { + subscriber.putCompletion() return } if let degradedValue = info[PHImageResultIsDegradedKey] as? Bool, degradedValue { diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index c01d1bbda6..6deed187a5 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -125,23 +125,27 @@ final class MediaPickerGridItemNode: GridItemNode { override init() { self.backgroundNode = ASImageNode() self.backgroundNode.contentMode = .scaleToFill + self.backgroundNode.isLayerBacked = true self.imageNode = ImageNode() self.imageNode.clipsToBounds = true self.imageNode.contentMode = .scaleAspectFill - self.imageNode.isLayerBacked = false + self.imageNode.isLayerBacked = true self.imageNode.animateFirstTransition = false self.gradientNode = ASImageNode() self.gradientNode.displaysAsynchronously = false self.gradientNode.displayWithoutProcessing = true self.gradientNode.image = maskImage + self.gradientNode.isLayerBacked = true self.typeIconNode = ASImageNode() self.typeIconNode.displaysAsynchronously = false self.typeIconNode.displayWithoutProcessing = true + self.typeIconNode.isLayerBacked = true self.durationNode = ImmediateTextNode() + self.durationNode.isLayerBacked = true self.draftNode = ImmediateTextNode() 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 = editedSignal |> mapToSignal { result in if let result = result { @@ -519,18 +523,22 @@ final class MediaPickerGridItemNode: GridItemNode { self.addSubnode(self.typeIconNode) self.setNeedsLayout() } - } else if asset.mediaType == .video { - if asset.mediaSubtypes.contains(.videoHighFrameRate) { - self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaSlomo") - } else if asset.mediaSubtypes.contains(.videoTimelapse) { - self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaTimelapse") - } else { - self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaVideo") + } + + if asset.mediaType == .video { + if !asset.isFavorite { + if asset.mediaSubtypes.contains(.videoHighFrameRate) { + self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaSlomo") + } 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) - if self.typeIconNode.supernode == nil { + if self.durationNode.supernode == nil { self.addSubnode(self.gradientNode) self.addSubnode(self.typeIconNode) 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)) 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.typeIconNode.frame = CGRect(x: 0.0, y: self.bounds.height - 20.0, width: 19.0, height: 19.0) self.activateAreaNode.frame = self.bounds @@ -619,11 +627,11 @@ final class MediaPickerGridItemNode: GridItemNode { func transitionView(snapshot: Bool) -> UIView { 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) return view } else { - return self.imageNode.view + return self.view } } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 3952ce379b..0bf1a5b204 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -478,7 +478,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { let throttledContentOffsetSignal = self.fastScrollContentOffset.get() |> mapToThrottled { next -> Signal 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 |> deliverOnMainQueue).start(next: { [weak self] contentOffset in @@ -1266,7 +1266,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { 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 { return } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index e5357adea0..119159e2a9 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -464,13 +464,16 @@ private final class StickerPackContainer: ASDisplayNode { var menuItems: [ContextMenuItem] = [] if let (info, _, _) = strongSelf.currentStickerPack, info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { if strongSelf.sendSticker != nil { + var iconName: String let actionTitle: String if let title = strongSelf.controller?.actionTitle { actionTitle = title + iconName = "Chat/Context Menu/Add" } else { 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 animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode.view, animationNode.bounds) diff --git a/submodules/TelegramUI/Components/CameraScreen/BUILD b/submodules/TelegramUI/Components/CameraScreen/BUILD index 234468828b..641269c171 100644 --- a/submodules/TelegramUI/Components/CameraScreen/BUILD +++ b/submodules/TelegramUI/Components/CameraScreen/BUILD @@ -71,12 +71,16 @@ swift_library( "//submodules/Components/BlurredBackgroundComponent", "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", "//submodules/Components/BundleIconComponent:BundleIconComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TooltipUI", "//submodules/TelegramUI/Components/MediaEditor", "//submodules/Components/MetalImageView", "//submodules/TelegramUI/Components/CameraButtonComponent", "//submodules/Utils/VolumeButtons", "//submodules/TelegramNotices", + "//submodules/DeviceAccess", + ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 794c373982..5d62b7da7f 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -21,6 +21,7 @@ import BundleIconComponent import CameraButtonComponent import VolumeButtons import TelegramNotices +import DeviceAccess let videoRedColor = UIColor(rgb: 0xff3b30) @@ -87,6 +88,8 @@ private final class CameraScreenComponent: CombinedComponent { let context: AccountContext let cameraState: CameraState + let cameraAuthorizationStatus: AccessType + let microphoneAuthorizationStatus: AccessType let hasAppeared: Bool let isVisible: Bool let panelWidth: CGFloat @@ -101,6 +104,8 @@ private final class CameraScreenComponent: CombinedComponent { init( context: AccountContext, cameraState: CameraState, + cameraAuthorizationStatus: AccessType, + microphoneAuthorizationStatus: AccessType, hasAppeared: Bool, isVisible: Bool, panelWidth: CGFloat, @@ -114,6 +119,8 @@ private final class CameraScreenComponent: CombinedComponent { ) { self.context = context self.cameraState = cameraState + self.cameraAuthorizationStatus = cameraAuthorizationStatus + self.microphoneAuthorizationStatus = microphoneAuthorizationStatus self.hasAppeared = hasAppeared self.isVisible = isVisible self.panelWidth = panelWidth @@ -133,6 +140,12 @@ private final class CameraScreenComponent: CombinedComponent { if lhs.cameraState != rhs.cameraState { return false } + if lhs.cameraAuthorizationStatus != rhs.cameraAuthorizationStatus { + return false + } + if lhs.microphoneAuthorizationStatus != rhs.microphoneAuthorizationStatus { + return false + } if lhs.hasAppeared != rhs.hasAppeared { return false } @@ -166,11 +179,7 @@ private final class CameraScreenComponent: CombinedComponent { return image } } - - private var cameraAuthorizationStatus: AVAuthorizationStatus = .notDetermined - private var microphoneAuthorizationStatus: AVAuthorizationStatus = .notDetermined - private var galleryAuthorizationStatus: PHAuthorizationStatus = .notDetermined - + private let context: AccountContext private let present: (ViewController) -> Void private let completion: ActionSlot> @@ -180,7 +189,7 @@ private final class CameraScreenComponent: CombinedComponent { private let getController: () -> CameraScreen? private var resultDisposable = MetaDisposable() - + private var mediaAssetsContext: MediaAssetsContext? fileprivate var lastGalleryAsset: PHAsset? private var lastGalleryAssetsDisposable: Disposable? @@ -502,6 +511,7 @@ private final class CameraScreenComponent: CombinedComponent { } static var body: Body { + let placeholder = Child(PlaceholderComponent.self) let cancelButton = Child(CameraButton.self) let captureControls = Child(CaptureControlsComponent.self) let zoomControl = Child(ZoomComponent.self) @@ -535,12 +545,51 @@ private final class CameraScreenComponent: CombinedComponent { let panelWidth = min(component.panelWidth, 185.0) var controlsBottomInset: CGFloat = 0.0 + let previewHeight = floorToScreenPixels(availableSize.width * 1.77778) if !isTablet { - let previewHeight = floorToScreenPixels(availableSize.width * 1.77778) if availableSize.height < previewHeight + 30.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 { @@ -593,7 +642,8 @@ private final class CameraScreenComponent: CombinedComponent { let captureControls = captureControls.update( component: CaptureControlsComponent( isTablet: isTablet, - hasAppeared: component.hasAppeared, + hasAppeared: component.hasAppeared && hasAllRequiredAccess, + hasAccess: hasAllRequiredAccess, shutterState: shutterState, lastGalleryAsset: state.lastGalleryAsset, tag: captureControlsTag, @@ -739,51 +789,53 @@ private final class CameraScreenComponent: CombinedComponent { ) } - let flashButton = flashButton.update( - component: CameraButton( - 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( + if hasAllRequiredAccess { + let flashButton = flashButton.update( component: CameraButton( - content: AnyComponentWithIdentity( - id: "dual", - component: AnyComponent( - DualIconComponent(isSelected: component.cameraState.isDualCameraEnabled) - ) - ), + content: flashContentComponent, action: { [weak state] in if let state { - state.toggleDualCamera() + state.toggleFlashMode() } } - ).tagged(dualButtonTag), + ).tagged(flashButtonTag), 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)) + 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( + 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( component: CameraButton( content: AnyComponentWithIdentity( @@ -807,6 +859,8 @@ private final class CameraScreenComponent: CombinedComponent { ) context.add(flipButton .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 if isTablet { 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) { self.controller = controller @@ -1312,12 +1371,33 @@ public class CameraScreen: ViewController { } 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 { self.cameraStateDisposable?.dispose() self.changingPositionDisposable?.dispose() self.idleTimerExtensionDisposable.dispose() + self.authorizationStatusDisposables.dispose() } private var pipPanGestureRecognizer: UIPanGestureRecognizer? @@ -1346,11 +1426,23 @@ public class CameraScreen: ViewController { pipPanGestureRecognizer.delegate = self self.previewContainerView.addGestureRecognizer(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 { return } @@ -1461,6 +1553,10 @@ public class CameraScreen: ViewController { camera.startCapture() self.camera = camera + + if self.hasAppeared { + self.maybePresentTooltips() + } } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { @@ -1581,7 +1677,7 @@ public class CameraScreen: ViewController { CATransaction.begin() CATransaction.setDisableActions(true) - self.requestUpdateLayout(hasAppeared: false, transition: .immediate) + self.requestUpdateLayout(hasAppeared: self.hasAppeared, transition: .immediate) CATransaction.commit() 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 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) { let _ = ApplicationSpecificNotice.incrementStoriesCameraTip(accountManager: accountManager).start() + Queue.mainQueue().justDispatch { + self?.maybePresentTooltips() + } return .dismiss(consume: true) } return .ignore @@ -2006,7 +2105,11 @@ public class CameraScreen: ViewController { self.hasAppeared = hasAppeared 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( @@ -2015,6 +2118,8 @@ public class CameraScreen: ViewController { CameraScreenComponent( context: self.context, cameraState: self.cameraState, + cameraAuthorizationStatus: self.cameraAuthorizationStatus, + microphoneAuthorizationStatus: self.microphoneAuthorizationStatus, hasAppeared: self.hasAppeared, isVisible: self.cameraIsActive && !self.hasGallery, panelWidth: panelWidth, diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift index 02adc9bcfe..d98fcf0b56 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift @@ -446,6 +446,7 @@ final class CaptureControlsComponent: Component { let isTablet: Bool let hasAppeared: Bool + let hasAccess: Bool let shutterState: ShutterButtonState let lastGalleryAsset: PHAsset? let tag: AnyObject? @@ -463,6 +464,7 @@ final class CaptureControlsComponent: Component { init( isTablet: Bool, hasAppeared: Bool, + hasAccess: Bool, shutterState: ShutterButtonState, lastGalleryAsset: PHAsset?, tag: AnyObject?, @@ -479,6 +481,7 @@ final class CaptureControlsComponent: Component { ) { self.isTablet = isTablet self.hasAppeared = hasAppeared + self.hasAccess = hasAccess self.shutterState = shutterState self.lastGalleryAsset = lastGalleryAsset self.tag = tag @@ -501,6 +504,9 @@ final class CaptureControlsComponent: Component { if lhs.hasAppeared != rhs.hasAppeared { return false } + if lhs.hasAccess != rhs.hasAccess { + return false + } if lhs.shutterState != rhs.shutterState { return false } @@ -944,7 +950,7 @@ final class CaptureControlsComponent: Component { 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 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) } + let alpha: CGFloat = component.hasAccess ? 1.0 : 0.3 transition.setBounds(view: shutterButtonView, bounds: CGRect(origin: .zero, size: shutterButtonFrame.size)) transition.setPosition(view: shutterButtonView, position: shutterButtonFrame.center) 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 { diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/PlaceholderComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/PlaceholderComponent.swift new file mode 100644 index 0000000000..f6c936cc6c --- /dev/null +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/PlaceholderComponent.swift @@ -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() + private let title = ComponentView() + private let text = ComponentView() + private let button = ComponentView() + + 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, 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, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift index 1624458c36..60f2659e89 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift @@ -221,7 +221,10 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { if let strongSelf = self { strongSelf.view.window?.endEditing(true) 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 { return strongSelf.interaction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, []) } else { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift index ca0bf0cbda..bd8832f41d 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift @@ -14,8 +14,13 @@ private func fullEntityMediaPath(_ path: String) -> String { public final class DrawingStickerEntity: DrawingEntity, Codable { public enum Content: Equatable { + public enum ImageType: Equatable { + case sticker + case rectangle + case dualPhoto + } case file(TelegramMediaFile) - case image(UIImage, Bool) + case image(UIImage, ImageType) case video(String, UIImage?, Bool) case dualVideoReference @@ -27,9 +32,9 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { } else { return false } - case let .image(lhsImage, lhsIsRectangle): - if case let .image(rhsImage, rhsIsRectangle) = rhs { - return lhsImage === rhsImage && lhsIsRectangle == rhsIsRectangle + case let .image(lhsImage, lhsImageType): + if case let .image(rhsImage, rhsImageType) = rhs { + return lhsImage === rhsImage && lhsImageType == rhsImageType } else { return false } @@ -56,6 +61,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { case videoImagePath case videoMirrored case isRectangle + case isDualPhoto case dualVideo case referenceDrawingSize case position @@ -121,8 +127,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { public var isRectangle: Bool { switch self.content { - case let .image(_, isRectangle): - return isRectangle + case let .image(_, imageType): + return imageType == .rectangle default: return false } @@ -157,7 +163,16 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { self.content = .file(file) } 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 - 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) { var imageValue: UIImage? 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 { case let .file(file): try container.encode(file, forKey: .file) - case let .image(image, isRectangle): + case let .image(image, imageType): let imagePath = "\(self.uuid).png" let fullImagePath = fullEntityMediaPath(imagePath) if let imageData = image.pngData() { @@ -190,7 +205,14 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { try? imageData.write(to: URL(fileURLWithPath: fullImagePath)) 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): try container.encode(path, forKey: .videoPath) let imagePath = "\(self.uuid).jpg" diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 727aaaddb5..8177b5c6ed 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -481,7 +481,8 @@ public final class MediaEditor { if player == nil { self.updateRenderChain() - self.maybeGeneratePersonSegmentation(image) + let _ = image +// self.maybeGeneratePersonSegmentation(image) } if let player { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index b8b1188581..2e99ced89c 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1167,13 +1167,13 @@ final class MediaEditorScreenComponent: Component { switch data { case let .sticker(image, _): 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) self.deactivateInput() } case let .images(images): 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) self.deactivateInput() } @@ -1925,8 +1925,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate 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)) } - }) - let imageEntity = DrawingStickerEntity(content: .image(image ?? additionalImage, false)) + }, scale: 1.0) + let imageEntity = DrawingStickerEntity(content: .image(image ?? additionalImage, .dualPhoto)) imageEntity.referenceDrawingSize = storyDimensions imageEntity.scale = 1.625 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 if let self, let image { 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 { 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 { - 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?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in 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 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?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() @@ -4110,7 +4110,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } let images = imageItems as! [UIImage] 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) } } } diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index a1d1028200..e7200776fe 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -978,7 +978,7 @@ final class ShareWithPeersScreenComponent: Component { } 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 emptyAnimationHeight = 148.0 let topInset: CGFloat = topOffset + itemLayout.containerInset + 40.0 @@ -1280,10 +1280,14 @@ final class ShareWithPeersScreenComponent: Component { ) 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 { 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 self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self else { @@ -1335,7 +1339,7 @@ final class ShareWithPeersScreenComponent: Component { sideInset: sideInset, title: "Name", peer: nil, - subtitle: "sub", + subtitle: self.searchStateContext != nil ? "" : "sub", subtitleAccessory: .none, presence: nil, selectionState: .editing(isSelected: false, isTinted: false), @@ -1753,7 +1757,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { case stories(editing: Bool) case chats case contacts(EngineStoryPrivacy.Base) - case search(String) + case search(query: String, onlyContacts: Bool) } fileprivate var stateValue: State? @@ -1889,7 +1893,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { self.readySubject.set(true) }) - case let .search(query): + case let .search(query, _): self.stateDisposable = (context.engine.contacts.searchLocalPeers(query: query) |> deliverOnMainQueue).start(next: { [weak self] peers in guard let self else {