Merge commit '076129348bdc195950533edc8d8d9fdbe2312ad7'

This commit is contained in:
Ali 2023-01-03 17:44:18 +04:00
commit 2c761951b2
16 changed files with 478 additions and 1087 deletions

View File

@ -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",
],
)

View File

@ -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()
}
}
}

View File

@ -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<String?, NoError> 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<CameraCode?>(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)
}
}

View File

@ -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<DrawingView.Action>
private let updateToolState: ActionSlot<DrawingToolState>
private let updateSelectedEntity: ActionSlot<DrawingEntity?>
private let insertEntity: ActionSlot<DrawingEntity>
fileprivate let insertEntity: ActionSlot<DrawingEntity>
private let deselectEntity: ActionSlot<Void>
private let updateEntitiesPlayback: ActionSlot<Bool>
private let previewBrushSize: ActionSlot<CGFloat?>
@ -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)
}
}

View File

@ -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()

View File

@ -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(

View File

@ -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
}

View File

@ -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<Double>()
@ -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<AnimatedStickerFrameQueue>(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<AnimatedStickerFrameQueue>(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))
}
}

View File

@ -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() {

View File

@ -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",

View File

@ -9,7 +9,6 @@ import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import AuthTransferUI
import ItemListPeerActionItem
import DeviceAccess
import QrCodeUI

View File

@ -9,7 +9,6 @@ import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import AuthTransferUI
import ItemListPeerActionItem
import DeviceAccess
import QrCodeUI

View File

@ -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",

View File

@ -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<Int, NoError> 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

View File

@ -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

View File

@ -42,7 +42,6 @@ import ChatListUI
import CallListUI
import AccountUtils
import PassportUI
import AuthTransferUI
import DeviceAccess
import LegacyMediaPickerUI
import TelegramNotices