diff --git a/submodules/AuthTransferUI/BUILD b/submodules/AuthTransferUI/BUILD deleted file mode 100644 index 1dd094a4db..0000000000 --- a/submodules/AuthTransferUI/BUILD +++ /dev/null @@ -1,37 +0,0 @@ -load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") - -swift_library( - name = "AuthTransferUI", - module_name = "AuthTransferUI", - srcs = glob([ - "Sources/**/*.swift", - ]), - copts = [ - "-warnings-as-errors", - ], - deps = [ - "//submodules/TelegramCore:TelegramCore", - "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/AsyncDisplayKit:AsyncDisplayKit", - "//submodules/Display:Display", - "//submodules/TelegramPresentationData:TelegramPresentationData", - "//submodules/AccountContext:AccountContext", - "//submodules/QrCode:QrCode", - "//submodules/Camera:Camera", - "//submodules/GlassButtonNode:GlassButtonNode", - "//submodules/AlertUI:AlertUI", - "//submodules/AppBundle:AppBundle", - "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", - "//submodules/AnimatedStickerNode:AnimatedStickerNode", - "//submodules/AnimationUI:AnimationUI", - "//submodules/PresentationDataUtils:PresentationDataUtils", - "//submodules/DeviceAccess:DeviceAccess", - "//submodules/UndoUI:UndoUI", - "//submodules/TextFormat:TextFormat", - "//submodules/Markdown:Markdown", - "//submodules/QrCodeUI:QrCodeUI", - ], - visibility = [ - "//visibility:public", - ], -) diff --git a/submodules/AuthTransferUI/Sources/AuthTransferConfirmationScreen.swift b/submodules/AuthTransferUI/Sources/AuthTransferConfirmationScreen.swift deleted file mode 100644 index 9588ef451e..0000000000 --- a/submodules/AuthTransferUI/Sources/AuthTransferConfirmationScreen.swift +++ /dev/null @@ -1,343 +0,0 @@ -import Foundation -import UIKit -import AppBundle -import AsyncDisplayKit -import Display -import SolidRoundedButtonNode -import SwiftSignalKit -import AnimationUI -import AccountContext -import TelegramPresentationData -import PresentationDataUtils -import TelegramCore -import Markdown -import DeviceAccess -import QrCodeUI - -private func transformedWithTheme(data: Data, theme: PresentationTheme) -> Data { - return transformedWithColors(data: data, colors: [(UIColor(rgb: 0x333333), theme.list.itemPrimaryTextColor.mixedWith(.white, alpha: 0.2)), (UIColor(rgb: 0xFFFFFF), theme.list.plainBackgroundColor), (UIColor(rgb: 0x50A7EA), theme.list.itemAccentColor), (UIColor(rgb: 0x212121), theme.list.plainBackgroundColor)]) -} - -public final class AuthDataTransferSplashScreen: ViewController { - private let context: AccountContext - private let activeSessionsContext: ActiveSessionsContext - private var presentationData: PresentationData - - public init(context: AccountContext, activeSessionsContext: ActiveSessionsContext) { - self.context = context - self.activeSessionsContext = activeSessionsContext - - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - - let defaultTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme) - let navigationBarTheme = NavigationBarTheme(buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: defaultTheme.badgeBackgroundColor, badgeStrokeColor: defaultTheme.badgeStrokeColor, badgeTextColor: defaultTheme.badgeTextColor) - - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Common_Back, close: self.presentationData.strings.Common_Close))) - - self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - self.navigationPresentation = .modalInLargeLayout - self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) - self.navigationBar?.intrinsicCanTransitionInline = false - - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - } - - override public func loadDisplayNode() { - self.displayNode = AuthDataTransferSplashScreenNode(context: self.context, presentationData: self.presentationData, action: { [weak self] in - guard let strongSelf = self else { - return - } - - DeviceAccess.authorizeAccess(to: .camera(.qrCode), presentationData: strongSelf.presentationData, present: { c, a in - guard let strongSelf = self else { - return - } - c.presentationArguments = a - strongSelf.context.sharedContext.mainWindow?.present(c, on: .root) - }, openSettings: { - self?.context.sharedContext.applicationBindings.openSettings() - }, { granted in - guard let strongSelf = self else { - return - } - guard granted else { - return - } - (strongSelf.navigationController as? NavigationController)?.replaceController(strongSelf, with: QrCodeScanScreen(context: strongSelf.context, subject: .authTransfer(activeSessionsContext: strongSelf.activeSessionsContext)), animated: true) - }) - }) - - self.displayNodeDidLoad() - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - (self.displayNode as! AuthDataTransferSplashScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) - } -} - -private final class AuthDataTransferSplashScreenNode: ViewControllerTracingNode { - private var presentationData: PresentationData - - private var animationSize: CGSize = CGSize() - private var animationOffset: CGPoint = CGPoint() - private let animationNode: AnimationNode? - private let titleNode: ImmediateTextNode - private let badgeBackgroundNodes: [ASImageNode] - private let badgeTextNodes: [ImmediateTextNode] - private let textNodes: [ImmediateTextNode] - let buttonNode: SolidRoundedButtonNode - - private let hierarchyTrackingNode: HierarchyTrackingNode - - var inProgress: Bool = false { - didSet { - self.buttonNode.isUserInteractionEnabled = !self.inProgress - self.buttonNode.alpha = self.inProgress ? 0.6 : 1.0 - } - } - - private var validLayout: ContainerViewLayout? - - init(context: AccountContext, presentationData: PresentationData, action: @escaping () -> Void) { - self.presentationData = presentationData - - if let url = getAppBundle().url(forResource: "anim_qr", withExtension: "json"), let data = try? Data(contentsOf: url) { - self.animationNode = AnimationNode(animationData: transformedWithTheme(data: data, theme: presentationData.theme)) - } else { - self.animationNode = nil - } - - let buttonText: String - - let badgeFont = Font.with(size: 13.0, design: .round, weight: .bold) - let textFont = Font.regular(16.0) - let textColor = self.presentationData.theme.list.itemPrimaryTextColor - - var badgeBackgroundNodes: [ASImageNode] = [] - var badgeTextNodes: [ImmediateTextNode] = [] - var textNodes: [ImmediateTextNode] = [] - - let badgeBackground = generateFilledCircleImage(diameter: 20.0, color: self.presentationData.theme.list.itemCheckColors.fillColor) - - for i in 0 ..< 3 { - let badgeBackgroundNode = ASImageNode() - badgeBackgroundNode.displaysAsynchronously = false - badgeBackgroundNode.displayWithoutProcessing = true - badgeBackgroundNode.image = badgeBackground - badgeBackgroundNodes.append(badgeBackgroundNode) - - let badgeTextNode = ImmediateTextNode() - badgeTextNode.displaysAsynchronously = false - badgeTextNode.attributedText = NSAttributedString(string: "\(i + 1)", font: badgeFont, textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .natural) - badgeTextNode.maximumNumberOfLines = 0 - badgeTextNode.lineSpacing = 0.1 - badgeTextNodes.append(badgeTextNode) - - let string: String - switch i { - case 0: - string = self.presentationData.strings.AuthSessions_AddDeviceIntro_Text1 - case 1: - string = self.presentationData.strings.AuthSessions_AddDeviceIntro_Text2 - default: - string = self.presentationData.strings.AuthSessions_AddDeviceIntro_Text3 - } - - let body = MarkdownAttributeSet(font: textFont, textColor: textColor) - let link = MarkdownAttributeSet(font: textFont, textColor: self.presentationData.theme.list.itemAccentColor, additionalAttributes: ["URL": true as NSNumber]) - - let text = parseMarkdownIntoAttributedString(string, attributes: MarkdownAttributes(body: body, bold: body, link: link, linkAttribute: { _ in - return nil - })) - - let textNode = ImmediateTextNode() - textNode.displaysAsynchronously = false - textNode.attributedText = text - textNode.maximumNumberOfLines = 0 - textNode.lineSpacing = 0.1 - textNodes.append(textNode) - } - - self.badgeBackgroundNodes = badgeBackgroundNodes - self.badgeTextNodes = badgeTextNodes - self.textNodes = textNodes - - buttonText = self.presentationData.strings.AuthSessions_AddDeviceIntro_Action - - self.titleNode = ImmediateTextNode() - self.titleNode.displaysAsynchronously = false - self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_AddDeviceIntro_Title, font: Font.bold(24.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) - self.titleNode.maximumNumberOfLines = 0 - self.titleNode.textAlignment = .center - - self.buttonNode = SolidRoundedButtonNode(title: buttonText, theme: SolidRoundedButtonTheme(backgroundColor: self.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor), height: 50.0, cornerRadius: 10.0, gloss: false) - self.buttonNode.isHidden = buttonText.isEmpty - - var updateInHierarchy: ((Bool) -> Void)? - self.hierarchyTrackingNode = HierarchyTrackingNode({ value in - updateInHierarchy?(value) - }) - - super.init() - - self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor - - self.addSubnode(self.hierarchyTrackingNode) - - if let animationNode = self.animationNode { - self.addSubnode(animationNode) - } - self.addSubnode(self.titleNode) - - self.badgeBackgroundNodes.forEach(self.addSubnode) - self.badgeTextNodes.forEach(self.addSubnode) - self.textNodes.forEach(self.addSubnode) - - self.addSubnode(self.buttonNode) - - self.buttonNode.pressed = { - action() - } - - for textNode in self.textNodes { - textNode.linkHighlightColor = self.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.5) - textNode.highlightAttributeAction = { attributes in - if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { - return NSAttributedString.Key(rawValue: "URL") - } else { - return nil - } - } - textNode.tapAttributeAction = { attributes, _ in - if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { - context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://desktop.telegram.org", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) - } - } - } - - updateInHierarchy = { [weak self] value in - if value { - self?.animationNode?.play() - } else { - self?.animationNode?.reset() - } - } - } - - override func didLoad() { - super.didLoad() - } - - func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { - let firstTime = self.validLayout == nil - self.validLayout = layout - - let sideInset: CGFloat = 22.0 - let textSideInset: CGFloat = 54.0 - let buttonSideInset: CGFloat = 16.0 - let titleSpacing: CGFloat = 25.0 - let buttonHeight: CGFloat = 50.0 - let buttonSpacing: CGFloat = 16.0 - let textSpacing: CGFloat = 25.0 - let badgeSize: CGFloat = 20.0 - - let animationFitSize = CGSize(width: min(500.0, layout.size.width - sideInset + 20.0), height: 500.0) - let animationSize = self.animationNode?.preferredSize()?.fitted(animationFitSize) ?? animationFitSize - let iconSize: CGSize = animationSize - let iconOffset = CGPoint() - - let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height)) - - var badgeTextSizes: [CGSize] = [] - var textSizes: [CGSize] = [] - var textContentHeight: CGFloat = 0.0 - for i in 0 ..< self.badgeTextNodes.count { - let badgeTextSize = self.badgeTextNodes[i].updateLayout(CGSize(width: 100.0, height: .greatestFiniteMagnitude)) - badgeTextSizes.append(badgeTextSize) - let textSize = self.textNodes[i].updateLayout(CGSize(width: layout.size.width - sideInset * 2.0 - 40.0, height: .greatestFiniteMagnitude)) - textSizes.append(textSize) - - if i != 0 { - textContentHeight += textSpacing - } - textContentHeight += textSize.height - } - - var contentHeight = iconSize.height + titleSize.height + titleSpacing + textContentHeight - - let bottomInset = layout.intrinsicInsets.bottom + 20.0 - let contentTopInset = navigationHeight - let contentBottomInset = bottomInset + buttonHeight + buttonSpacing - - let iconSpacing: CGFloat = max(20.0, min(61.0, layout.size.height - contentTopInset - contentBottomInset - contentHeight - 40.0)) - - contentHeight += iconSpacing - - var contentVerticalOrigin = contentTopInset + floor((layout.size.height - contentTopInset - contentBottomInset - contentHeight) / 2.0) - - let buttonWidth = layout.size.width - buttonSideInset * 2.0 - - let buttonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonWidth) / 2.0), y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: buttonWidth, height: buttonHeight)) - transition.updateFrame(node: self.buttonNode, frame: buttonFrame) - let _ = self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition) - - let maxContentVerticalOrigin = buttonFrame.minY - 12.0 - contentHeight - - contentVerticalOrigin = min(contentVerticalOrigin, maxContentVerticalOrigin) - - var contentY = contentVerticalOrigin - let iconFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0) + self.animationOffset.x, y: contentY), size: iconSize).offsetBy(dx: iconOffset.x, dy: iconOffset.y) - contentY += iconSize.height + iconSpacing - if let animationNode = self.animationNode { - transition.updateFrameAdditive(node: animationNode, frame: iconFrame) - if iconFrame.minY < 0.0 { - transition.updateAlpha(node: animationNode, alpha: 0.0) - } else { - transition.updateAlpha(node: animationNode, alpha: 1.0) - } - } - - let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: contentY), size: titleSize) - transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame) - contentY += titleSize.height + titleSpacing - - for i in 0 ..< self.badgeTextNodes.count { - if i != 0 { - contentY += textSpacing - } - - let badgeTextSize = badgeTextSizes[i] - let textSize = textSizes[i] - - let textFrame = CGRect(origin: CGPoint(x: textSideInset, y: contentY), size: textSize) - transition.updateFrameAdditive(node: self.textNodes[i], frame: textFrame) - - let badgeFrame = CGRect(origin: CGPoint(x: sideInset, y: textFrame.minY), size: CGSize(width: badgeSize, height: badgeSize)) - transition.updateFrameAdditive(node: self.badgeBackgroundNodes[i], frame: badgeFrame) - - let badgeTextOffsetX: CGFloat - if i == 0 { - badgeTextOffsetX = 0.5 - } else { - badgeTextOffsetX = 1.0 - } - - transition.updateFrameAdditive(node: self.badgeTextNodes[i], frame: CGRect(origin: CGPoint(x: badgeFrame.minX + floor((badgeFrame.width - badgeTextSize.width) / 2.0) + badgeTextOffsetX, y: badgeFrame.minY + floor((badgeFrame.height - badgeTextSize.height) / 2.0) + 0.5), size: badgeTextSize)) - - contentY += textSize.height - } - - if firstTime { - self.animationNode?.play() - } - } -} diff --git a/submodules/AuthTransferUI/Sources/AuthTransferScanScreen.swift b/submodules/AuthTransferUI/Sources/AuthTransferScanScreen.swift deleted file mode 100644 index ff89bba3a6..0000000000 --- a/submodules/AuthTransferUI/Sources/AuthTransferScanScreen.swift +++ /dev/null @@ -1,544 +0,0 @@ -import Foundation -import UIKit -import AccountContext -import AsyncDisplayKit -import Display -import SwiftSignalKit -import Camera -import GlassButtonNode -import CoreImage -import AlertUI -import TelegramPresentationData -import TelegramCore -import UndoUI -import Markdown -import TextFormat - -private func parseAuthTransferUrl(_ url: URL) -> Data? { - var tokenString: String? - if let query = url.query, let components = URLComponents(string: "/?" + query), let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "token", !value.isEmpty { - tokenString = value - } - } - } - } - if var tokenString = tokenString { - tokenString = tokenString.replacingOccurrences(of: "-", with: "+") - tokenString = tokenString.replacingOccurrences(of: "_", with: "/") - while tokenString.count % 4 != 0 { - tokenString.append("=") - } - if let data = Data(base64Encoded: tokenString) { - return data - } - } - return nil -} - -private func generateFrameImage() -> UIImage? { - return generateImage(CGSize(width: 64.0, height: 64.0), contextGenerator: { size, context in - let bounds = CGRect(origin: CGPoint(), size: size) - context.clear(bounds) - context.setStrokeColor(UIColor.white.cgColor) - context.setLineWidth(4.0) - context.setLineCap(.round) - - let path = CGMutablePath() - path.move(to: CGPoint(x: 2.0, y: 2.0 + 26.0)) - path.addArc(tangent1End: CGPoint(x: 2.0, y: 2.0), tangent2End: CGPoint(x: 2.0 + 26.0, y: 2.0), radius: 6.0) - path.addLine(to: CGPoint(x: 2.0 + 26.0, y: 2.0)) - context.addPath(path) - context.strokePath() - - path.move(to: CGPoint(x: size.width - 2.0, y: 2.0 + 26.0)) - path.addArc(tangent1End: CGPoint(x: size.width - 2.0, y: 2.0), tangent2End: CGPoint(x: 2.0 + 26.0, y: 2.0), radius: 6.0) - path.addLine(to: CGPoint(x: size.width - 2.0 - 26.0, y: 2.0)) - context.addPath(path) - context.strokePath() - - path.move(to: CGPoint(x: 2.0, y: size.height - 2.0 - 26.0)) - path.addArc(tangent1End: CGPoint(x: 2.0, y: size.height - 2.0), tangent2End: CGPoint(x: 2.0 + 26.0, y: size.height - 2.0), radius: 6.0) - path.addLine(to: CGPoint(x: 2.0 + 26.0, y: size.height - 2.0)) - context.addPath(path) - context.strokePath() - - path.move(to: CGPoint(x: size.width - 2.0, y: size.height - 2.0 - 26.0)) - path.addArc(tangent1End: CGPoint(x: size.width - 2.0, y: size.height - 2.0), tangent2End: CGPoint(x: 2.0 + 26.0, y: size.height - 2.0), radius: 6.0) - path.addLine(to: CGPoint(x: size.width - 2.0 - 26.0, y: size.height - 2.0)) - context.addPath(path) - context.strokePath() - })?.stretchableImage(withLeftCapWidth: 32, topCapHeight: 32) -} - -public final class AuthTransferScanScreen: ViewController { - private let context: AccountContext - private let activeSessionsContext: ActiveSessionsContext - private var presentationData: PresentationData - - private var codeDisposable: Disposable? - private var inForegroundDisposable: Disposable? - private let approveDisposable = MetaDisposable() - - private var controllerNode: AuthTransferScanScreenNode { - return self.displayNode as! AuthTransferScanScreenNode - } - - public init(context: AccountContext, activeSessionsContext: ActiveSessionsContext) { - self.context = context - self.activeSessionsContext = activeSessionsContext - - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - - let navigationBarTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: .white, primaryTextColor: .white, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear) - - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Common_Back, close: self.presentationData.strings.Common_Close))) - - self.statusBar.statusBarStyle = .White - - self.navigationPresentation = .modalInLargeLayout - self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) - self.navigationBar?.intrinsicCanTransitionInline = false - - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) - - self.inForegroundDisposable = (context.sharedContext.applicationBindings.applicationInForeground - |> deliverOnMainQueue).start(next: { [weak self] inForeground in - guard let strongSelf = self else { - return - } - (strongSelf.displayNode as! AuthTransferScanScreenNode).updateInForeground(inForeground) - }) - - #if DEBUG - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Test", style: .plain, target: self, action: #selector(self.testPressed)) - #endif - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.codeDisposable?.dispose() - self.inForegroundDisposable?.dispose() - self.approveDisposable.dispose() - } - - @objc private func testPressed() { - self.dismissWithSuccess(session: nil) - } - - private func dismissWithSuccess(session: RecentAccountSession?) { - if let navigationController = navigationController as? NavigationController { - let activeSessionsContext = self.activeSessionsContext - - self.present(UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: self.presentationData.strings.AuthSessions_AddedDeviceTitle, text: session?.appName ?? "Telegram for macOS", cancel: self.presentationData.strings.AuthSessions_AddedDeviceTerminate), elevatedLayout: false, animateInAsReplacement: false, action: { value in - if value == .undo, let session = session { - let _ = activeSessionsContext.remove(hash: session.hash).start() - return true - } else { - return false - } - }), in: .window(.root)) - - var viewControllers = navigationController.viewControllers - viewControllers = viewControllers.filter { controller in - if controller is RecentSessionsController { - return false - } - if controller === self { - return false - } - return true - } - viewControllers.append(self.context.sharedContext.makeRecentSessionsController(context: self.context, activeSessionsContext: activeSessionsContext)) - navigationController.setViewControllers(viewControllers, animated: true) - } else { - self.dismiss() - } - } - - override public func loadDisplayNode() { - self.displayNode = AuthTransferScanScreenNode(context: self.context, presentationData: self.presentationData) - - self.displayNodeDidLoad() - - self.codeDisposable = ((self.displayNode as! AuthTransferScanScreenNode).focusedCode.get() - |> map { code -> String? in - return code?.message - } - |> distinctUntilChanged - |> mapToSignal { code -> Signal in - return .single(code) - |> delay(0.5, queue: Queue.mainQueue()) - }).start(next: { [weak self] code in - guard let strongSelf = self else { - return - } - guard let code = code else { - return - } - if let url = URL(string: code), let parsedToken = parseAuthTransferUrl(url) { - strongSelf.approveDisposable.set((approveAuthTransferToken(account: strongSelf.context.account, token: parsedToken, activeSessionsContext: strongSelf.activeSessionsContext) - |> deliverOnMainQueue).start(next: { session in - guard let strongSelf = self else { - return - } - strongSelf.controllerNode.codeWithError = nil - let activeSessionsContext = strongSelf.activeSessionsContext - Queue.mainQueue().after(1.5, { - activeSessionsContext.loadMore() - }) - strongSelf.dismissWithSuccess(session: session) - }, error: { _ in - guard let strongSelf = self else { - return - } - strongSelf.controllerNode.codeWithError = code - strongSelf.controllerNode.updateFocusedRect(nil) - })) - } - }) - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - (self.displayNode as! AuthTransferScanScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) - } -} - -private final class AuthTransferScanScreenNode: ViewControllerTracingNode, UIScrollViewDelegate { - private let context: AccountContext - private var presentationData: PresentationData - - private let previewNode: CameraPreviewNode - private let fadeNode: ASDisplayNode - private let topDimNode: ASDisplayNode - private let bottomDimNode: ASDisplayNode - private let leftDimNode: ASDisplayNode - private let rightDimNode: ASDisplayNode - private let centerDimNode: ASDisplayNode - private let frameNode: ASImageNode - private let torchButtonNode: GlassButtonNode - private let titleNode: ImmediateTextNode - private let textNode: ImmediateTextNode - private let errorTextNode: ImmediateTextNode - - private let camera: Camera - private let codeDisposable = MetaDisposable() - private var torchDisposable: Disposable? - - fileprivate let focusedCode = ValuePromise(ignoreRepeated: true) - private var focusedRect: CGRect? - - private var validLayout: (ContainerViewLayout, CGFloat)? - - var codeWithError: String? { - didSet { - if self.codeWithError != oldValue { - if self.codeWithError != nil { - self.errorTextNode.isHidden = false - } else { - self.errorTextNode.isHidden = true - } - } - } - } - - private var highlightViews: [UIVisualEffectView] = [] - - init(context: AccountContext, presentationData: PresentationData) { - self.context = context - self.presentationData = presentationData - - self.previewNode = CameraPreviewNode() - self.previewNode.backgroundColor = .black - - self.fadeNode = ASDisplayNode() - self.fadeNode.alpha = 0.0 - self.fadeNode.backgroundColor = .black - - self.topDimNode = ASDisplayNode() - self.topDimNode.alpha = 0.625 - self.topDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8) - - self.bottomDimNode = ASDisplayNode() - self.bottomDimNode.alpha = 0.625 - self.bottomDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8) - - self.leftDimNode = ASDisplayNode() - self.leftDimNode.alpha = 0.625 - self.leftDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8) - - self.rightDimNode = ASDisplayNode() - self.rightDimNode.alpha = 0.625 - self.rightDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8) - - self.centerDimNode = ASDisplayNode() - self.centerDimNode.alpha = 0.0 - self.centerDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8) - - self.frameNode = ASImageNode() - self.frameNode.image = generateFrameImage() - - self.torchButtonNode = GlassButtonNode(icon: UIImage(bundleImageName: "Wallet/CameraFlashIcon")!, label: nil) - - self.titleNode = ImmediateTextNode() - self.titleNode.displaysAsynchronously = false - self.titleNode.attributedText = NSAttributedString(string: presentationData.strings.AuthSessions_AddDevice_ScanTitle, font: Font.bold(32.0), textColor: .white) - self.titleNode.maximumNumberOfLines = 0 - self.titleNode.textAlignment = .center - - let textFont = Font.regular(17.0) - let boldFont = Font.bold(17.0) - - var text = presentationData.strings.AuthSessions_AddDevice_ScanInstallInfo - text = text.replacingOccurrences(of: " [", with: " [").replacingOccurrences(of: ") ", with: ") ") - - let attributedText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: .white), bold: MarkdownAttributeSet(font: boldFont, textColor: .white), link: MarkdownAttributeSet(font: boldFont, textColor: .white), linkAttribute: { contents in - return (TelegramTextAttributes.URL, contents) - }))) - - self.textNode = ImmediateTextNode() - self.textNode.displaysAsynchronously = false - self.textNode.attributedText = attributedText - self.textNode.maximumNumberOfLines = 0 - self.textNode.textAlignment = .center - self.textNode.lineSpacing = 0.5 - - self.errorTextNode = ImmediateTextNode() - self.errorTextNode.displaysAsynchronously = false - self.errorTextNode.attributedText = NSAttributedString(string: presentationData.strings.AuthSessions_AddDevice_InvalidQRCode, font: Font.medium(16.0), textColor: .white) - self.errorTextNode.maximumNumberOfLines = 0 - self.errorTextNode.textAlignment = .center - self.errorTextNode.isHidden = true - - self.camera = Camera(configuration: .init(preset: .hd1920x1080, position: .back, audio: false)) - - super.init() - - self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor - - self.torchDisposable = (self.camera.hasTorch - |> deliverOnMainQueue).start(next: { [weak self] hasTorch in - if let strongSelf = self { - strongSelf.torchButtonNode.isHidden = !hasTorch - } - }) - - self.addSubnode(self.previewNode) - self.addSubnode(self.fadeNode) - self.addSubnode(self.topDimNode) - self.addSubnode(self.bottomDimNode) - self.addSubnode(self.leftDimNode) - self.addSubnode(self.rightDimNode) - self.addSubnode(self.centerDimNode) - self.addSubnode(self.frameNode) - self.addSubnode(self.torchButtonNode) - self.addSubnode(self.titleNode) - self.addSubnode(self.textNode) - self.addSubnode(self.errorTextNode) - - self.torchButtonNode.addTarget(self, action: #selector(self.torchPressed), forControlEvents: .touchUpInside) - } - - deinit { - self.codeDisposable.dispose() - self.torchDisposable?.dispose() - self.camera.stopCapture(invalidate: true) - } - - fileprivate func updateInForeground(_ inForeground: Bool) { - if !inForeground { - self.camera.stopCapture(invalidate: false) - } else { - self.camera.startCapture() - } - } - - override func didLoad() { - super.didLoad() - - self.camera.attachPreviewNode(self.previewNode) - self.camera.startCapture() - - let throttledSignal = self.camera.detectedCodes - |> mapToThrottled { next -> Signal<[CameraCode], NoError> in - return .single(next) |> then(.complete() |> delay(0.3, queue: Queue.concurrentDefaultQueue())) - } - - self.codeDisposable.set((throttledSignal - |> deliverOnMainQueue).start(next: { [weak self] codes in - guard let strongSelf = self else { - return - } - let filteredCodes = codes.filter { $0.message.hasPrefix("tg://") } - if let code = filteredCodes.first, CGRect(x: 0.3, y: 0.3, width: 0.4, height: 0.4).contains(code.boundingBox.center) { - if strongSelf.codeWithError != code.message { - strongSelf.codeWithError = nil - } - if strongSelf.codeWithError == code.message { - strongSelf.focusedCode.set(nil) - strongSelf.updateFocusedRect(nil) - } else { - strongSelf.focusedCode.set(code) - strongSelf.updateFocusedRect(code.boundingBox) - } - } else { - strongSelf.codeWithError = nil - strongSelf.focusedCode.set(nil) - strongSelf.updateFocusedRect(nil) - } - })) - - let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) - recognizer.tapActionAtPoint = { _ in - return .waitForSingleTap - } - self.textNode.view.addGestureRecognizer(recognizer) - } - - @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { - switch recognizer.state { - case .ended: - if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { - switch gesture { - case .tap: - if let (_, attributes) = self.textNode.attributesAtPoint(location) { - if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - switch url { - case "desktop": - self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: "https://getdesktop.telegram.org", forceExternal: true, presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) - case "web": - self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: "https://web.telegram.org", forceExternal: true, presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) - default: - break - } - } - } - default: - break - } - } - default: - break - } - } - - func updateFocusedRect(_ rect: CGRect?) { - self.focusedRect = rect - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring)) - } - } - - func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { - self.validLayout = (layout, navigationHeight) - - let sideInset: CGFloat = 66.0 - let titleSpacing: CGFloat = 48.0 - let bounds = CGRect(origin: CGPoint(), size: layout.size) - - if case .tablet = layout.deviceMetrics.type { - if UIDevice.current.orientation == .landscapeLeft { - self.previewNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) - } else if UIDevice.current.orientation == .landscapeRight { - self.previewNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) - } else { - self.previewNode.transform = CATransform3DIdentity - } - } - transition.updateFrame(node: self.previewNode, frame: bounds) - transition.updateFrame(node: self.fadeNode, frame: bounds) - - let frameSide = max(240.0, layout.size.width - sideInset * 2.0) - let dimHeight = ceil((layout.size.height - frameSide) / 2.0) - let dimInset = (layout.size.width - frameSide) / 2.0 - - let dimAlpha: CGFloat - let dimRect: CGRect - let controlsAlpha: CGFloat - let centerDimAlpha: CGFloat = 0.0 - let frameAlpha: CGFloat = 1.0 - if let focusedRect = self.focusedRect { - controlsAlpha = 0.0 - dimAlpha = 1.0 - let side = max(bounds.width * focusedRect.width, bounds.height * focusedRect.height) * 0.6 - let center = CGPoint(x: (1.0 - focusedRect.center.y) * bounds.width, y: focusedRect.center.x * bounds.height) - dimRect = CGRect(x: center.x - side / 2.0, y: center.y - side / 2.0, width: side, height: side) - } else { - controlsAlpha = 1.0 - dimAlpha = 0.625 - dimRect = CGRect(x: dimInset, y: dimHeight, width: layout.size.width - dimInset * 2.0, height: layout.size.height - dimHeight * 2.0) - } - - transition.updateAlpha(node: self.topDimNode, alpha: dimAlpha) - transition.updateAlpha(node: self.bottomDimNode, alpha: dimAlpha) - transition.updateAlpha(node: self.leftDimNode, alpha: dimAlpha) - transition.updateAlpha(node: self.rightDimNode, alpha: dimAlpha) - transition.updateAlpha(node: self.centerDimNode, alpha: centerDimAlpha) - transition.updateAlpha(node: self.frameNode, alpha: frameAlpha) - - transition.updateFrame(node: self.topDimNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: dimRect.minY)) - transition.updateFrame(node: self.bottomDimNode, frame: CGRect(x: 0.0, y: dimRect.maxY, width: layout.size.width, height: max(0.0, layout.size.height - dimRect.maxY))) - transition.updateFrame(node: self.leftDimNode, frame: CGRect(x: 0.0, y: dimRect.minY, width: max(0.0, dimRect.minX), height: dimRect.height)) - transition.updateFrame(node: self.rightDimNode, frame: CGRect(x: dimRect.maxX, y: dimRect.minY, width: max(0.0, layout.size.width - dimRect.maxX), height: dimRect.height)) - transition.updateFrame(node: self.frameNode, frame: dimRect.insetBy(dx: -2.0, dy: -2.0)) - transition.updateFrame(node: self.centerDimNode, frame: dimRect) - - let buttonSize = CGSize(width: 72.0, height: 72.0) - var torchFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonSize.width) / 2.0), y: dimHeight + frameSide + 98.0), size: buttonSize) - let updatedTorchY = min(torchFrame.minY, layout.size.height - torchFrame.height - 10.0) - let additionalTorchOffset: CGFloat = updatedTorchY - torchFrame.minY - torchFrame.origin.y = updatedTorchY - transition.updateFrame(node: self.torchButtonNode, frame: torchFrame) - - transition.updateAlpha(node: self.textNode, alpha: controlsAlpha) - transition.updateAlpha(node: self.errorTextNode, alpha: controlsAlpha) - transition.updateAlpha(node: self.torchButtonNode, alpha: controlsAlpha) - for view in self.highlightViews { - transition.updateAlpha(layer: view.layer, alpha: controlsAlpha) - } - - let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - 16.0, height: layout.size.height)) - let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - 16.0, height: layout.size.height)) - let errorTextSize = self.errorTextNode.updateLayout(CGSize(width: layout.size.width - 16.0, height: layout.size.height)) - var textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: max(dimHeight - textSize.height - titleSpacing, navigationHeight + floorToScreenPixels((dimHeight - navigationHeight - textSize.height) / 2.0) + 5.0)), size: textSize) - let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: textFrame.minY - 18.0 - titleSize.height), size: titleSize) - if titleFrame.minY < navigationHeight { - transition.updateAlpha(node: self.titleNode, alpha: 0.0) - textFrame = textFrame.offsetBy(dx: 0.0, dy: -5.0) - } else { - transition.updateAlpha(node: self.titleNode, alpha: controlsAlpha) - } - var errorTextFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - errorTextSize.width) / 2.0), y: dimHeight + frameSide + 48.0), size: errorTextSize) - errorTextFrame.origin.y += floor(additionalTorchOffset / 2.0) - - transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame) - transition.updateFrameAdditive(node: self.textNode, frame: textFrame) - transition.updateFrameAdditive(node: self.errorTextNode, frame: errorTextFrame) - - if self.highlightViews.isEmpty { - let urlAttributesAndRects = self.textNode.cachedLayout?.allAttributeRects(name: "UrlAttributeT") ?? [] - - for (_, rect) in urlAttributesAndRects { - let view = UIVisualEffectView(effect: UIBlurEffect(style: .light)) - view.clipsToBounds = true - view.layer.cornerRadius = 5.0 - view.frame = rect.offsetBy(dx: self.textNode.frame.minX, dy: self.textNode.frame.minY).insetBy(dx: -4.0, dy: -2.0) - self.view.insertSubview(view, belowSubview: self.textNode.view) - self.highlightViews.append(view) - } - } - } - - @objc private func torchPressed() { - self.torchButtonNode.isSelected = !self.torchButtonNode.isSelected - self.camera.setTorchActive(self.torchButtonNode.isSelected) - } -} - diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index b70a9509ea..bf9f321c04 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import CoreServices import Display import ComponentFlow import LegacyComponents @@ -921,7 +922,7 @@ private final class DrawingScreenComponent: CombinedComponent { } func addTextEntity() { - let textEntity = DrawingTextEntity(text: NSAttributedString(), style: .regular, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: DrawingColor(color: .white)) + let textEntity = DrawingTextEntity(text: NSAttributedString(), style: .regular, animation: .none, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: DrawingColor(color: .white)) self.insertEntity.invoke(textEntity) } @@ -934,7 +935,7 @@ private final class DrawingScreenComponent: CombinedComponent { self?.updateEntitiesPlayback.invoke(true) if let file = file { - let stickerEntity = DrawingStickerEntity(file: file) + let stickerEntity = DrawingStickerEntity(content: .file(file)) self?.insertEntity.invoke(stickerEntity) } else { self?.updateCurrentMode(.drawing) @@ -1085,6 +1086,7 @@ private final class DrawingScreenComponent: CombinedComponent { component: TextSettingsComponent( color: nil, style: DrawingTextStyle(style: textEntity.style), + animation: DrawingTextAnimation(animation: textEntity.animation), alignment: DrawingTextAlignment(alignment: textEntity.alignment), font: DrawingTextFont(font: textEntity.font), isEmojiKeyboard: false, @@ -1111,6 +1113,27 @@ private final class DrawingScreenComponent: CombinedComponent { } state?.updated(transition: .easeInOut(duration: 0.2)) }, + toggleAnimation: { [weak state, weak textEntity] in + guard let textEntity = textEntity else { + return + } + var nextAnimation: DrawingTextEntity.Animation + switch textEntity.animation { + case .none: + nextAnimation = .typing + case .typing: + nextAnimation = .wiggle + case .wiggle: + nextAnimation = .zoomIn + case .zoomIn: + nextAnimation = .none + } + textEntity.animation = nextAnimation + if let entityView = textEntity.currentEntityView { + entityView.update() + } + state?.updated(transition: .easeInOut(duration: 0.2)) + }, toggleAlignment: { [weak state, weak textEntity] in guard let textEntity = textEntity else { return @@ -1933,7 +1956,7 @@ private final class DrawingScreenComponent: CombinedComponent { } } -public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { +public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, UIDropInteractionDelegate { fileprivate final class Node: ViewControllerTracingNode { private weak var controller: DrawingScreen? private let context: AccountContext @@ -1942,7 +1965,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { private let performAction: ActionSlot private let updateToolState: ActionSlot private let updateSelectedEntity: ActionSlot - private let insertEntity: ActionSlot + fileprivate let insertEntity: ActionSlot private let deselectEntity: ActionSlot private let updateEntitiesPlayback: ActionSlot private let previewBrushSize: ActionSlot @@ -2683,6 +2706,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { TextSettingsComponent( color: textEntity.color, style: DrawingTextStyle(style: textEntity.style), + animation: DrawingTextAnimation(animation: textEntity.animation), alignment: DrawingTextAlignment(alignment: textEntity.alignment), font: DrawingTextFont(font: textEntity.font), isEmojiKeyboard: entityView.textView.inputView != nil, @@ -2731,6 +2755,29 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate) } }, + toggleAnimation: { [weak self] in + self?.dismissFontPicker() + guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { + return + } + var nextAnimation: DrawingTextEntity.Animation + switch textEntity.animation { + case .none: + nextAnimation = .typing + case .typing: + nextAnimation = .wiggle + case .wiggle: + nextAnimation = .zoomIn + case .zoomIn: + nextAnimation = .none + } + textEntity.animation = nextAnimation + entityView.update() + + if let (layout, orientation) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate) + } + }, toggleAlignment: { [weak self] in self?.dismissFontPicker() guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { @@ -2893,6 +2940,9 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { self.displayNode = Node(controller: self, context: self.context) super.displayNodeDidLoad() + + let dropInteraction = UIDropInteraction(delegate: self) + self.view.addInteraction(dropInteraction) } public func generateResultData() -> TGPaintingData? { @@ -2944,15 +2994,17 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { var stickers: [Any] = [] for entity in self.entitiesView.entities { - if let sticker = entity as? DrawingStickerEntity { + if let sticker = entity as? DrawingStickerEntity, case let .file(file) = sticker.content { let coder = PostboxEncoder() - coder.encodeRootObject(sticker.file) + coder.encodeRootObject(file) stickers.append(coder.makeData()) } else if let text = entity as? DrawingTextEntity, let subEntities = text.renderSubEntities { for sticker in subEntities { - let coder = PostboxEncoder() - coder.encodeRootObject(sticker.file) - stickers.append(coder.makeData()) + if case let .file(file) = sticker.content { + let coder = PostboxEncoder() + coder.encodeRootObject(file) + stickers.append(coder.makeData()) + } } } } @@ -3004,4 +3056,44 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { self.orientation = orientation self.containerLayoutUpdated(layout, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate) } + + @available(iOSApplicationExtension 11.0, iOS 11.0, *) + public func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool { + return session.hasItemsConforming(toTypeIdentifiers: [kUTTypeImage as String]) + } + + @available(iOSApplicationExtension 11.0, iOS 11.0, *) + public func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal { + //self.chatDisplayNode.updateDropInteraction(isActive: true) + + let operation: UIDropOperation + operation = .copy + return UIDropProposal(operation: operation) + } + + @available(iOSApplicationExtension 11.0, iOS 11.0, *) + public func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) { + session.loadObjects(ofClass: UIImage.self) { [weak self] imageItems in + guard let strongSelf = self else { + return + } + let images = imageItems as! [UIImage] + + //strongSelf.chatDisplayNode.updateDropInteraction(isActive: false) + if images.count == 1, let image = images.first, max(image.size.width, image.size.height) > 1.0 { + let entity = DrawingStickerEntity(content: .image(image)) + strongSelf.node.insertEntity.invoke(entity) + } + } + } + + @available(iOSApplicationExtension 11.0, iOS 11.0, *) + public func dropInteraction(_ interaction: UIDropInteraction, sessionDidExit session: UIDropSession) { + //self.chatDisplayNode.updateDropInteraction(isActive: false) + } + + @available(iOSApplicationExtension 11.0, iOS 11.0, *) + public func dropInteraction(_ interaction: UIDropInteraction, sessionDidEnd session: UIDropSession) { + //self.chatDisplayNode.updateDropInteraction(isActive: false) + } } diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift index 5f829a9e97..de673e2367 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift @@ -9,10 +9,14 @@ import StickerResources import AccountContext public final class DrawingStickerEntity: DrawingEntity, Codable { + public enum Content { + case file(TelegramMediaFile) + case image(UIImage) + } private enum CodingKeys: String, CodingKey { case uuid - case isAnimated case file + case image case referenceDrawingSize case position case scale @@ -21,8 +25,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { } public let uuid: UUID - public let isAnimated: Bool - public let file: TelegramMediaFile + public let content: Content public var referenceDrawingSize: CGSize public var position: CGPoint @@ -38,15 +41,22 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { } public var baseSize: CGSize { - let size = max(10.0, min(self.referenceDrawingSize.width, self.referenceDrawingSize.height) * 0.4) + let size = max(10.0, min(self.referenceDrawingSize.width, self.referenceDrawingSize.height) * 0.2) return CGSize(width: size, height: size) } - init(file: TelegramMediaFile) { + public var isAnimated: Bool { + switch self.content { + case let .file(file): + return file.isAnimatedSticker || file.isVideoSticker + case .image: + return false + } + } + + init(content: Content) { self.uuid = UUID() - self.isAnimated = file.isAnimatedSticker || file.isVideoSticker - - self.file = file + self.content = content self.referenceDrawingSize = .zero self.position = CGPoint() @@ -58,8 +68,13 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.uuid = try container.decode(UUID.self, forKey: .uuid) - self.isAnimated = try container.decode(Bool.self, forKey: .isAnimated) - self.file = try container.decode(TelegramMediaFile.self, forKey: .file) + if let file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .file) { + self.content = .file(file) + } else if let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) { + self.content = .image(image) + } else { + fatalError() + } self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) self.position = try container.decode(CGPoint.self, forKey: .position) self.scale = try container.decode(CGFloat.self, forKey: .scale) @@ -70,8 +85,12 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.uuid, forKey: .uuid) - try container.encode(self.isAnimated, forKey: .isAnimated) - try container.encode(self.file, forKey: .file) + switch self.content { + case let .file(file): + try container.encode(file, forKey: .file) + case let .image(image): + try container.encodeIfPresent(image.pngData(), forKey: .image) + } try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) try container.encode(self.position, forKey: .position) try container.encode(self.scale, forKey: .scale) @@ -80,7 +99,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { } public func duplicate() -> DrawingEntity { - let newEntity = DrawingStickerEntity(file: self.file) + let newEntity = DrawingStickerEntity(content: self.content) newEntity.referenceDrawingSize = self.referenceDrawingSize newEntity.position = self.position newEntity.scale = self.scale @@ -108,7 +127,6 @@ final class DrawingStickerEntityView: DrawingEntityView { var started: ((Double) -> Void)? private var currentSize: CGSize? - private var dimensions: CGSize? private let imageNode: TransformImageNode private var animationNode: AnimatedStickerNode? @@ -139,46 +157,77 @@ final class DrawingStickerEntityView: DrawingEntityView { self.cachedDisposable.dispose() } - private var file: TelegramMediaFile { - return (self.entity as! DrawingStickerEntity).file + private var file: TelegramMediaFile? { + if case let .file(file) = self.stickerEntity.content { + return file + } else { + return nil + } + } + + private var image: UIImage? { + if case let .image(image) = self.stickerEntity.content { + return image + } else { + return nil + } + } + + private var dimensions: CGSize { + switch self.stickerEntity.content { + case let .file(file): + return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) + case let .image(image): + return image.size + } } private func setup() { - if let dimensions = self.file.dimensions { - if self.file.isAnimatedSticker || self.file.isVideoSticker || self.file.mimeType == "video/webm" { - if self.animationNode == nil { - let animationNode = DefaultAnimatedStickerNodeImpl() - animationNode.autoplay = false - self.animationNode = animationNode - animationNode.started = { [weak self, weak animationNode] in - self?.imageNode.isHidden = true - - if let animationNode = animationNode { - let _ = (animationNode.status - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] status in - self?.started?(status.duration) - }) + if let file = self.file { + if let dimensions = file.dimensions { + if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" { + if self.animationNode == nil { + let animationNode = DefaultAnimatedStickerNodeImpl() + animationNode.autoplay = false + self.animationNode = animationNode + animationNode.started = { [weak self, weak animationNode] in + self?.imageNode.isHidden = true + + if let animationNode = animationNode { + let _ = (animationNode.status + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] status in + self?.started?(status.duration) + }) + } } + self.addSubnode(animationNode) } - self.addSubnode(animationNode) + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, userLocation: .other, file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 256.0, height: 256.0)))) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: file.resource).start()) + } else { + if let animationNode = self.animationNode { + animationNode.visibility = false + self.animationNode = nil + animationNode.removeFromSupernode() + self.imageNode.isHidden = false + self.didSetUpAnimationNode = false + } + self.imageNode.setSignal(chatMessageSticker(account: self.context.account, userLocation: .other, file: file, small: false, synchronousLoad: false)) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: false)).start()) } - let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512) - self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, userLocation: .other, file: self.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 256.0, height: 256.0)))) - self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(self.file), resource: self.file.resource).start()) - } else { - if let animationNode = self.animationNode { - animationNode.visibility = false - self.animationNode = nil - animationNode.removeFromSupernode() - self.imageNode.isHidden = false - self.didSetUpAnimationNode = false - } - self.imageNode.setSignal(chatMessageSticker(account: self.context.account, userLocation: .other, file: self.file, small: false, synchronousLoad: false)) - self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(self.file), resource: chatMessageStickerResource(file: self.file, small: false)).start()) + self.setNeedsLayout() } - - self.dimensions = dimensions.cgSize + } else if let image = self.image { + self.imageNode.setSignal(.single({ arguments -> DrawingContext? in + let context = DrawingContext(size: arguments.drawingSize, opaque: false, clear: true) + context?.withFlippedContext({ ctx in + if let cgImage = image.cgImage { + ctx.draw(cgImage, in: CGRect(origin: .zero, size: arguments.drawingSize)) + } + }) + return context + })) self.setNeedsLayout() } } @@ -215,15 +264,17 @@ final class DrawingStickerEntityView: DrawingEntityView { if self.isPlaying != isPlaying { self.isPlaying = isPlaying - if isPlaying && !self.didSetUpAnimationNode { - self.didSetUpAnimationNode = true - let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512) - let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)) - let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.file.resource, isVideo: self.file.isVideoSticker || self.file.mimeType == "video/webm") - self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) - - self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384) - |> deliverOn(Queue.concurrentDefaultQueue())).start()) + if let file = self.file { + if isPlaying && !self.didSetUpAnimationNode { + self.didSetUpAnimationNode = true + let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) + let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)) + let source = AnimatedStickerResourceSource(account: self.context.account, resource: file.resource, isVideo: file.isVideoSticker || file.mimeType == "video/webm") + self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) + + self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384) + |> deliverOn(Queue.concurrentDefaultQueue())).start()) + } } self.animationNode?.visibility = isPlaying } @@ -241,33 +292,29 @@ final class DrawingStickerEntityView: DrawingEntityView { let sideSize: CGFloat = size.width let boundingSize = CGSize(width: sideSize, height: sideSize) - if let dimensions = self.dimensions { - let imageSize = dimensions.aspectFitted(boundingSize) - self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) - if let animationNode = self.animationNode { - animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) - animationNode.updateLayout(size: imageSize) - - if !self.didApplyVisibility { - self.didApplyVisibility = true - self.applyVisibility() - } + + let imageSize = self.dimensions.aspectFitted(boundingSize) + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() + self.imageNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) + if let animationNode = self.animationNode { + animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) + animationNode.updateLayout(size: imageSize) + + if !self.didApplyVisibility { + self.didApplyVisibility = true + self.applyVisibility() } - self.update(animated: false) } + self.update(animated: false) } } - + override func update(animated: Bool) { - guard let dimensions = self.stickerEntity.file.dimensions?.cgSize else { - return - } self.center = self.stickerEntity.position let size = self.stickerEntity.baseSize - self.bounds = CGRect(origin: .zero, size: dimensions.aspectFitted(size)) + self.bounds = CGRect(origin: .zero, size: self.dimensions.aspectFitted(size)) self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.stickerEntity.rotation), self.stickerEntity.scale, self.stickerEntity.scale) var transform = CATransform3DIdentity @@ -297,13 +344,13 @@ final class DrawingStickerEntityView: DrawingEntityView { self.pushIdentityTransformForMeasurement() selectionView.transform = .identity - let bounds = self.selectionBounds - let center = bounds.center + let maxSide = max(self.selectionBounds.width, self.selectionBounds.height) + let center = self.selectionBounds.center let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0 selectionView.center = self.convert(center, to: selectionView.superview) - selectionView.bounds = CGRect(origin: .zero, size: CGSize(width: (bounds.width * self.stickerEntity.scale) * scale + selectionView.selectionInset * 2.0, height: (bounds.height * self.stickerEntity.scale) * scale + selectionView.selectionInset * 2.0)) + selectionView.bounds = CGRect(origin: .zero, size: CGSize(width: (maxSide * self.stickerEntity.scale) * scale + selectionView.selectionInset * 2.0, height: (maxSide * self.stickerEntity.scale) * scale + selectionView.selectionInset * 2.0)) selectionView.transform = CGAffineTransformMakeRotation(self.stickerEntity.rotation) self.popIdentityTransformForMeasurement() diff --git a/submodules/DrawingUI/Sources/DrawingTextEntity.swift b/submodules/DrawingUI/Sources/DrawingTextEntity.swift index 21b63efde0..c14555de0d 100644 --- a/submodules/DrawingUI/Sources/DrawingTextEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingTextEntity.swift @@ -43,6 +43,7 @@ public final class DrawingTextEntity: DrawingEntity, Codable { case text case textAttributes case style + case animation case font case alignment case fontSize @@ -61,19 +62,13 @@ public final class DrawingTextEntity: DrawingEntity, Codable { case filled case semi case stroke - - init(style: DrawingTextEntity.Style) { - switch style { - case .regular: - self = .regular - case .filled: - self = .filled - case .semi: - self = .semi - case .stroke: - self = .stroke - } - } + } + + enum Animation: Codable { + case none + case typing + case wiggle + case zoomIn } enum Font: Codable { @@ -100,6 +95,9 @@ public final class DrawingTextEntity: DrawingEntity, Codable { public var uuid: UUID public var isAnimated: Bool { + if self.animation != .none { + return true + } var isAnimated = false self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in if let _ = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { @@ -111,6 +109,7 @@ public final class DrawingTextEntity: DrawingEntity, Codable { var text: NSAttributedString var style: Style + var animation: Animation var font: Font var alignment: Alignment var fontSize: CGFloat @@ -130,11 +129,12 @@ public final class DrawingTextEntity: DrawingEntity, Codable { public var renderImage: UIImage? public var renderSubEntities: [DrawingStickerEntity]? - init(text: NSAttributedString, style: Style, font: Font, alignment: Alignment, fontSize: CGFloat, color: DrawingColor) { + init(text: NSAttributedString, style: Style, animation: Animation, font: Font, alignment: Alignment, fontSize: CGFloat, color: DrawingColor) { self.uuid = UUID() self.text = text self.style = style + self.animation = animation self.font = font self.alignment = alignment self.fontSize = fontSize @@ -160,6 +160,7 @@ public final class DrawingTextEntity: DrawingEntity, Codable { self.text = attributedString self.style = try container.decode(Style.self, forKey: .style) + self.animation = try container.decode(Animation.self, forKey: .animation) self.font = try container.decode(Font.self, forKey: .font) self.alignment = try container.decode(Alignment.self, forKey: .alignment) self.fontSize = try container.decode(CGFloat.self, forKey: .fontSize) @@ -191,6 +192,7 @@ public final class DrawingTextEntity: DrawingEntity, Codable { try container.encode(textAttributes, forKey: .textAttributes) try container.encode(self.style, forKey: .style) + try container.encode(self.animation, forKey: .animation) try container.encode(self.font, forKey: .font) try container.encode(self.alignment, forKey: .alignment) try container.encode(self.fontSize, forKey: .fontSize) @@ -210,7 +212,7 @@ public final class DrawingTextEntity: DrawingEntity, Codable { } public func duplicate() -> DrawingEntity { - let newEntity = DrawingTextEntity(text: self.text, style: self.style, font: self.font, alignment: self.alignment, fontSize: self.fontSize, color: self.color) + let newEntity = DrawingTextEntity(text: self.text, style: self.style, animation: self.animation, font: self.font, alignment: self.alignment, fontSize: self.fontSize, color: self.color) newEntity.referenceDrawingSize = self.referenceDrawingSize newEntity.position = self.position newEntity.width = self.width @@ -700,7 +702,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { } let emojiTextPosition = emojiRect.center.offsetBy(dx: -textSize.width / 2.0, dy: -textSize.height / 2.0) - let entity = DrawingStickerEntity(file: file) + let entity = DrawingStickerEntity(content: .file(file)) entity.referenceDrawingSize = CGSize(width: itemSize * 2.5, height: itemSize * 2.5) entity.scale = scale entity.position = textPosition.offsetBy( diff --git a/submodules/DrawingUI/Sources/TextSettingsComponent.swift b/submodules/DrawingUI/Sources/TextSettingsComponent.swift index efebcef577..10d8c0bc62 100644 --- a/submodules/DrawingUI/Sources/TextSettingsComponent.swift +++ b/submodules/DrawingUI/Sources/TextSettingsComponent.swift @@ -27,6 +27,26 @@ enum DrawingTextStyle: Equatable { } } +enum DrawingTextAnimation: Equatable { + case none + case typing + case wiggle + case zoomIn + + init(animation: DrawingTextEntity.Animation) { + switch animation { + case .none: + self = .none + case .typing: + self = .typing + case .wiggle: + self = .wiggle + case .zoomIn: + self = .zoomIn + } + } +} + enum DrawingTextAlignment: Equatable { case left case center @@ -266,6 +286,7 @@ final class TextFontComponent: Component { final class TextSettingsComponent: CombinedComponent { let color: DrawingColor? let style: DrawingTextStyle + let animation: DrawingTextAnimation let alignment: DrawingTextAlignment let font: DrawingTextFont let isEmojiKeyboard: Bool @@ -277,6 +298,7 @@ final class TextSettingsComponent: CombinedComponent { let updateFastColorPickerPan: (CGPoint) -> Void let dismissFastColorPicker: () -> Void let toggleStyle: () -> Void + let toggleAnimation: () -> Void let toggleAlignment: () -> Void let presentFontPicker: () -> Void let toggleKeyboard: (() -> Void)? @@ -284,6 +306,7 @@ final class TextSettingsComponent: CombinedComponent { init( color: DrawingColor?, style: DrawingTextStyle, + animation: DrawingTextAnimation, alignment: DrawingTextAlignment, font: DrawingTextFont, isEmojiKeyboard: Bool, @@ -294,12 +317,14 @@ final class TextSettingsComponent: CombinedComponent { updateFastColorPickerPan: @escaping (CGPoint) -> Void = { _ in }, dismissFastColorPicker: @escaping () -> Void = {}, toggleStyle: @escaping () -> Void, + toggleAnimation: @escaping () -> Void, toggleAlignment: @escaping () -> Void, presentFontPicker: @escaping () -> Void, toggleKeyboard: (() -> Void)? ) { self.color = color self.style = style + self.animation = animation self.alignment = alignment self.font = font self.isEmojiKeyboard = isEmojiKeyboard @@ -310,6 +335,7 @@ final class TextSettingsComponent: CombinedComponent { self.updateFastColorPickerPan = updateFastColorPickerPan self.dismissFastColorPicker = dismissFastColorPicker self.toggleStyle = toggleStyle + self.toggleAnimation = toggleAnimation self.toggleAlignment = toggleAlignment self.presentFontPicker = presentFontPicker self.toggleKeyboard = toggleKeyboard @@ -322,6 +348,9 @@ final class TextSettingsComponent: CombinedComponent { if lhs.style != rhs.style { return false } + if lhs.animation != rhs.animation { + return false + } if lhs.alignment != rhs.alignment { return false } diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index 336f9326f3..f0533842ef 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -75,7 +75,7 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity { } let account: Account - let file: TelegramMediaFile + let file: TelegramMediaFile? let entity: DrawingStickerEntity let animated: Bool let durationPromise = Promise() @@ -95,47 +95,53 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity { init(account: Account, entity: DrawingStickerEntity) { self.account = account self.entity = entity - self.file = entity.file - self.animated = file.isAnimatedSticker || file.isVideoSticker - - if file.isAnimatedSticker || file.isVideoSticker { - self.source = AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: file.isVideoSticker) - if let source = self.source { - let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512) - let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384, height: 384)) - self.disposables.add((source.cachedDataPath(width: Int(fittedDimensions.width), height: Int(fittedDimensions.height)) + self.animated = entity.isAnimated + + switch entity.content { + case let .file(file): + self.file = file + if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" { + self.source = AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: file.isVideoSticker) + if let source = self.source { + let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) + let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384, height: 384)) + self.disposables.add((source.cachedDataPath(width: Int(fittedDimensions.width), height: Int(fittedDimensions.height)) |> deliverOn(self.queue)).start(next: { [weak self] path, complete in - if let strongSelf = self, complete { - if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) { - let queue = strongSelf.queue - let frameSource = AnimatedStickerCachedFrameSource(queue: queue, data: data, complete: complete, notifyUpdated: {})! - strongSelf.frameCount = frameSource.frameCount - strongSelf.frameRate = frameSource.frameRate - - let duration = Double(frameSource.frameCount) / Double(frameSource.frameRate) - strongSelf.totalDuration = duration - - strongSelf.durationPromise.set(.single(duration)) - - let frameQueue = QueueLocalObject(queue: queue, generate: { - return AnimatedStickerFrameQueue(queue: queue, length: 1, source: frameSource) - }) - strongSelf.frameQueue.set(.single(frameQueue)) + if let strongSelf = self, complete { + if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) { + let queue = strongSelf.queue + let frameSource = AnimatedStickerCachedFrameSource(queue: queue, data: data, complete: complete, notifyUpdated: {})! + strongSelf.frameCount = frameSource.frameCount + strongSelf.frameRate = frameSource.frameRate + + let duration = Double(frameSource.frameCount) / Double(frameSource.frameRate) + strongSelf.totalDuration = duration + + strongSelf.durationPromise.set(.single(duration)) + + let frameQueue = QueueLocalObject(queue: queue, generate: { + return AnimatedStickerFrameQueue(queue: queue, length: 1, source: frameSource) + }) + strongSelf.frameQueue.set(.single(frameQueue)) + } + } + })) + } + } else { + self.disposables.add((chatMessageSticker(account: self.account, userLocation: .other, file: file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false) + |> deliverOn(self.queue)).start(next: { [weak self] generator in + if let strongSelf = self { + let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: entity.baseSize, boundingSize: entity.baseSize, intrinsicInsets: UIEdgeInsets())) + let image = context?.generateImage() + if let image = image { + strongSelf.imagePromise.set(.single(image)) } } })) } - } else { - self.disposables.add((chatMessageSticker(account: self.account, userLocation: .other, file: self.file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false) - |> deliverOn(self.queue)).start(next: { [weak self] generator in - if let strongSelf = self { - let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: entity.baseSize, boundingSize: entity.baseSize, intrinsicInsets: UIEdgeInsets())) - let image = context?.generateImage() - if let image = image { - strongSelf.imagePromise.set(.single(image)) - } - } - })) + case let .image(image): + self.file = nil + self.imagePromise.set(.single(image)) } } diff --git a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift index 04597be426..d38c8efc06 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift @@ -291,6 +291,99 @@ public final class QrCodeScanScreen: ViewController { } } +private final class FrameNode: ASDisplayNode { + let topLeftLine: CAShapeLayer + let topRightLine: CAShapeLayer + let bottomLeftLine: CAShapeLayer + let bottomRightLine: CAShapeLayer + + override init() { + self.topLeftLine = CAShapeLayer() + self.topRightLine = CAShapeLayer() + self.bottomLeftLine = CAShapeLayer() + self.bottomRightLine = CAShapeLayer() + + super.init() + + for line in self.lines { + line.strokeColor = UIColor.white.cgColor + line.fillColor = UIColor.clear.cgColor + line.lineWidth = 4.0 + line.lineCap = .round + self.layer.addSublayer(line) + } + } + + private var lines: [CAShapeLayer] { + return [ + self.topLeftLine, + self.topRightLine, + self.bottomLeftLine, + self.bottomRightLine + ] + } + + func animateIn() { + let strokeStart = self.topLeftLine.strokeStart + let strokeEnd = self.topLeftLine.strokeEnd + + let duration: Double = 0.85 + let delay: Double = 0.15 + + for line in self.lines { + line.animateSpring(from: 0.0 as NSNumber, to: strokeStart as NSNumber, keyPath: "strokeStart", duration: duration, delay: delay) + line.animateSpring(from: 1.0 as NSNumber, to: strokeEnd as NSNumber, keyPath: "strokeEnd", duration: duration, delay: delay) + } + } + + func updateLayout(size: CGSize) { + let cornerRadius: CGFloat = 6.0 + + let lineLength = size.width / 2.0 - cornerRadius + let targetLineLength = 24.0 + let fraction = targetLineLength / lineLength + let strokeFraction = (1.0 - fraction) / 2.0 + let strokeStart = strokeFraction + let strokeEnd = 1.0 - strokeFraction + + let topLeftPath = CGMutablePath() + topLeftPath.move(to: CGPoint(x: 0.0, y: size.height / 2.0)) + topLeftPath.addArc(center: CGPoint(x: cornerRadius, y: cornerRadius), radius: cornerRadius, startAngle: -.pi, endAngle: -.pi / 2.0, clockwise: false) + topLeftPath.addLine(to: CGPoint(x: size.width / 2.0, y: 0.0)) + self.topLeftLine.path = topLeftPath + self.topLeftLine.strokeStart = strokeStart + self.topLeftLine.strokeEnd = strokeEnd + + let topRightPath = CGMutablePath() + topRightPath.move(to: CGPoint(x: size.width / 2.0, y: 0.0)) + topRightPath.addArc(center: CGPoint(x: size.width - cornerRadius, y: cornerRadius), radius: cornerRadius, startAngle: -.pi / 2.0, endAngle: 0.0, clockwise: false) + topRightPath.addLine(to: CGPoint(x: size.width, y: size.height / 2.0)) + self.topRightLine.path = topRightPath + self.topRightLine.strokeStart = strokeStart + self.topRightLine.strokeEnd = strokeEnd + + let bottomRightPath = CGMutablePath() + bottomRightPath.move(to: CGPoint(x: size.width, y: size.height / 2.0)) + bottomRightPath.addArc(center: CGPoint(x: size.width - cornerRadius, y: size.height - cornerRadius), radius: cornerRadius, startAngle: 0.0, endAngle: .pi / 2.0, clockwise: false) + bottomRightPath.addLine(to: CGPoint(x: size.width / 2.0, y: size.height)) + self.bottomRightLine.path = bottomRightPath + self.bottomRightLine.strokeStart = strokeStart + self.bottomRightLine.strokeEnd = strokeEnd + + let bottomLeftPath = CGMutablePath() + bottomLeftPath.move(to: CGPoint(x: size.width / 2.0, y: size.height)) + bottomLeftPath.addArc(center: CGPoint(x: cornerRadius, y: size.height - cornerRadius), radius: cornerRadius, startAngle: .pi / 2.0, endAngle: .pi, clockwise: false) + bottomLeftPath.addLine(to: CGPoint(x: 0.0, y: size.height / 2.0)) + self.bottomLeftLine.path = bottomLeftPath + self.bottomLeftLine.strokeStart = strokeStart + self.bottomLeftLine.strokeEnd = strokeEnd + + for line in self.lines { + line.frame = CGRect(origin: .zero, size: size) + } + } +} + private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData @@ -304,7 +397,7 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie private let leftDimNode: ASDisplayNode private let rightDimNode: ASDisplayNode private let centerDimNode: ASDisplayNode - private let frameNode: ASImageNode + private let frameNode: FrameNode private let galleryButtonNode: GlassButtonNode private let torchButtonNode: GlassButtonNode private let titleNode: ImmediateTextNode @@ -350,28 +443,29 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie self.fadeNode.alpha = 0.0 self.fadeNode.backgroundColor = .black + let dimColor = UIColor(rgb: 0x000000, alpha: 0.8) + self.topDimNode = ASDisplayNode() self.topDimNode.alpha = 0.625 - self.topDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8) + self.topDimNode.backgroundColor = dimColor self.bottomDimNode = ASDisplayNode() self.bottomDimNode.alpha = 0.625 - self.bottomDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8) + self.bottomDimNode.backgroundColor = dimColor self.leftDimNode = ASDisplayNode() self.leftDimNode.alpha = 0.625 - self.leftDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8) + self.leftDimNode.backgroundColor = dimColor self.rightDimNode = ASDisplayNode() self.rightDimNode.alpha = 0.625 - self.rightDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8) + self.rightDimNode.backgroundColor = dimColor self.centerDimNode = ASDisplayNode() self.centerDimNode.alpha = 0.0 - self.centerDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8) + self.centerDimNode.backgroundColor = dimColor - self.frameNode = ASImageNode() - self.frameNode.image = generateFrameImage() + self.frameNode = FrameNode() self.galleryButtonNode = GlassButtonNode(icon: UIImage(bundleImageName: "Wallet/CameraGalleryIcon")!, label: nil) self.torchButtonNode = GlassButtonNode(icon: UIImage(bundleImageName: "Wallet/CameraFlashIcon")!, label: nil) @@ -517,6 +611,16 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie self.textNode.view.addGestureRecognizer(recognizer) } + private var animatedIn = false + func animateIn() { + guard !self.animatedIn else { + return + } + self.animatedIn = true + + self.frameNode.animateIn() + } + @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { switch recognizer.state { case .ended: @@ -551,9 +655,16 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie } } - func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { + private var animatingIn = false + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, animateIn: Bool = false, transition: ContainedViewLayoutTransition) { + let hadLayout = self.validLayout != nil self.validLayout = (layout, navigationHeight) + var prepareForAnimateIn = false + if !hadLayout { + prepareForAnimateIn = true + } + let sideInset: CGFloat = 66.0 let titleSpacing: CGFloat = 48.0 let bounds = CGRect(origin: CGPoint(), size: layout.size) @@ -571,11 +682,20 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie transition.updateFrame(node: self.fadeNode, frame: bounds) let frameSide = max(240.0, layout.size.width - sideInset * 2.0) + let animateInScale: CGFloat = 0.4 + var effectiveFrameSide = frameSide + if prepareForAnimateIn { + effectiveFrameSide = round(effectiveFrameSide * animateInScale) + } + let dimHeight = ceil((layout.size.height - frameSide) / 2.0) + let effectiveDimHeight = ceil((layout.size.height - effectiveFrameSide) / 2.0) let dimInset = (layout.size.width - frameSide) / 2.0 + let effectiveDimInset = (layout.size.width - effectiveFrameSide) / 2.0 let dimAlpha: CGFloat let dimRect: CGRect + let frameRect: CGRect let controlsAlpha: CGFloat let centerDimAlpha: CGFloat = 0.0 let frameAlpha: CGFloat = 1.0 @@ -585,10 +705,12 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie let side = max(bounds.width * focusedRect.width, bounds.height * focusedRect.height) * 0.6 let center = CGPoint(x: (1.0 - focusedRect.center.y) * bounds.width, y: focusedRect.center.x * bounds.height) dimRect = CGRect(x: center.x - side / 2.0, y: center.y - side / 2.0, width: side, height: side) + frameRect = dimRect } else { controlsAlpha = 1.0 dimAlpha = 0.625 - dimRect = CGRect(x: dimInset, y: dimHeight, width: layout.size.width - dimInset * 2.0, height: layout.size.height - dimHeight * 2.0) + dimRect = CGRect(x: effectiveDimInset, y: effectiveDimHeight, width: layout.size.width - effectiveDimInset * 2.0, height: layout.size.height - effectiveDimHeight * 2.0) + frameRect = CGRect(x: dimInset, y: dimHeight, width: layout.size.width - dimInset * 2.0, height: layout.size.height - dimHeight * 2.0) } transition.updateAlpha(node: self.topDimNode, alpha: dimAlpha) @@ -598,12 +720,25 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie transition.updateAlpha(node: self.centerDimNode, alpha: centerDimAlpha) transition.updateAlpha(node: self.frameNode, alpha: frameAlpha) - transition.updateFrame(node: self.topDimNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: dimRect.minY)) - transition.updateFrame(node: self.bottomDimNode, frame: CGRect(x: 0.0, y: dimRect.maxY, width: layout.size.width, height: max(0.0, layout.size.height - dimRect.maxY))) - transition.updateFrame(node: self.leftDimNode, frame: CGRect(x: 0.0, y: dimRect.minY, width: max(0.0, dimRect.minX), height: dimRect.height)) - transition.updateFrame(node: self.rightDimNode, frame: CGRect(x: dimRect.maxX, y: dimRect.minY, width: max(0.0, layout.size.width - dimRect.maxX), height: dimRect.height)) - transition.updateFrame(node: self.frameNode, frame: dimRect.insetBy(dx: -2.0, dy: -2.0)) - transition.updateFrame(node: self.centerDimNode, frame: dimRect) + if !self.animatingIn { + var delay: Double = 0.0 + if animateIn { + self.animatingIn = true + delay = 0.1 + } + transition.updateFrame(node: self.topDimNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: dimRect.minY), delay: delay, completion: { _ in + self.animatingIn = false + }) + transition.updateFrame(node: self.bottomDimNode, frame: CGRect(x: 0.0, y: dimRect.maxY, width: layout.size.width, height: max(0.0, layout.size.height - dimRect.maxY)), delay: delay) + transition.updateFrame(node: self.leftDimNode, frame: CGRect(x: 0.0, y: dimRect.minY, width: max(0.0, dimRect.minX), height: dimRect.height), delay: delay) + transition.updateFrame(node: self.rightDimNode, frame: CGRect(x: dimRect.maxX, y: dimRect.minY, width: max(0.0, layout.size.width - dimRect.maxX), height: dimRect.height), delay: delay) + transition.updateFrame(node: self.frameNode, frame: frameRect) + self.frameNode.updateLayout(size: frameRect.size) + transition.updateFrame(node: self.centerDimNode, frame: frameRect) + if animateIn { + transition.animateTransformScale(node: self.frameNode, from: CGPoint(x: animateInScale, y: animateInScale), delay: delay) + } + } let buttonSize = CGSize(width: 72.0, height: 72.0) var torchFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonSize.width) / 2.0), y: dimHeight + frameSide + 98.0), size: buttonSize) @@ -657,6 +792,11 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie self.highlightViews.append(view) } } + + if prepareForAnimateIn { + self.animateIn() + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, animateIn: true, transition: .animated(duration: 0.8, curve: .customSpring(damping: 88.0, initialVelocity: 0.0))) + } } @objc private func galleryPressed() { diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index 92705d888a..38f6451297 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -88,7 +88,6 @@ swift_library( "//submodules/PhoneNumberFormat:PhoneNumberFormat", "//submodules/OpenInExternalAppUI:OpenInExternalAppUI", "//submodules/AccountUtils:AccountUtils", - "//submodules/AuthTransferUI:AuthTransferUI", "//submodules/UIKitRuntimeUtils:UIKitRuntimeUtils", "//submodules/DebugSettingsUI:DebugSettingsUI", "//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode", diff --git a/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift index 55fbaead88..0492707e9b 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift @@ -9,7 +9,6 @@ import TelegramUIPreferences import ItemListUI import PresentationDataUtils import AccountContext -import AuthTransferUI import ItemListPeerActionItem import DeviceAccess import QrCodeUI diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift index db65fcdc95..293df2023a 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift @@ -9,7 +9,6 @@ import TelegramUIPreferences import ItemListUI import PresentationDataUtils import AccountContext -import AuthTransferUI import ItemListPeerActionItem import DeviceAccess import QrCodeUI diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 81ee929b36..2d2a8a7280 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -268,7 +268,6 @@ swift_library( "//submodules/Svg:Svg", "//submodules/ManagedAnimationNode:ManagedAnimationNode", "//submodules/TooltipUI:TooltipUI", - "//submodules/AuthTransferUI:AuthTransferUI", "//submodules/ListMessageItem:ListMessageItem", "//submodules/FileMediaResourceStatus:FileMediaResourceStatus", "//submodules/ChatMessageInteractiveMediaBadge:ChatMessageInteractiveMediaBadge", diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 334b8b739f..9b460cf5ef 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -9668,7 +9668,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let peerId = self.chatLocation.peerId if let subject = self.subject, case .scheduledMessages = subject { } else { - self.buttonUnreadCountDisposable = (self.context.chatLocationUnreadCount(for: self.chatLocation, contextHolder: self.chatLocationContextHolder) + let throttledUnreadCountSignal = self.context.chatLocationUnreadCount(for: self.chatLocation, contextHolder: self.chatLocationContextHolder) + |> mapToThrottled { value -> Signal in + return .single(value) |> then(.complete() |> delay(0.2, queue: Queue.mainQueue())) + } + self.buttonUnreadCountDisposable = (throttledUnreadCountSignal |> deliverOnMainQueue).start(next: { [weak self] count in guard let strongSelf = self else { return diff --git a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift index fd6d9a92da..98ab11bdc2 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift @@ -6,7 +6,7 @@ import TelegramPresentationData import WallpaperBackgroundNode import AnimatedCountLabelNode -private let badgeFont = Font.regular(13.0) +private let badgeFont = Font.with(size: 13.0, traits: [.monospacedNumbers]) enum ChatHistoryNavigationButtonType { case down @@ -197,7 +197,7 @@ class ChatHistoryNavigationButtonNode: ContextControllerSourceNode { } }) self.badgeBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } else if previousValue != self.currentValue { + } else if previousValue < self.currentValue { self.badgeBackgroundNode.layer.animateScale(from: 1.0, to: 1.2, duration: 0.12, removeOnCompletion: false, completion: { [weak self] finished in if let strongSelf = self { strongSelf.badgeBackgroundNode.layer.animateScale(from: 1.2, to: 1.0, duration: 0.12, removeOnCompletion: false, completion: { _ in diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 5fcc48a636..eb195fc3a0 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -42,7 +42,6 @@ import ChatListUI import CallListUI import AccountUtils import PassportUI -import AuthTransferUI import DeviceAccess import LegacyMediaPickerUI import TelegramNotices