Temporary QR codes

This commit is contained in:
Ilya Laktyushin 2022-11-27 15:00:10 +04:00
parent dc4a04daa7
commit 3caedf5670
10 changed files with 298 additions and 66 deletions

Binary file not shown.

View File

@ -79,6 +79,7 @@ public class AnimatedCountLabelNode: ASDisplayNode {
fileprivate var resolvedSegments: [ResolvedSegment.Key: (ResolvedSegment, TextNode)] = [:]
public var reverseAnimationDirection: Bool = false
public var alwaysOneDirection: Bool = false
override public init() {
super.init()
@ -91,6 +92,7 @@ public class AnimatedCountLabelNode: ASDisplayNode {
segmentLayouts[segmentKey] = TextNode.asyncLayout(segmentAndTextNode.1)
}
let reverseAnimationDirection = self.reverseAnimationDirection
let alwaysOneDirection = self.alwaysOneDirection
return { [weak self] size, initialSegments in
var segments: [ResolvedSegment] = []
@ -173,7 +175,7 @@ public class AnimatedCountLabelNode: ASDisplayNode {
fromAlpha = CGFloat(presentation.opacity)
}
var offsetY: CGFloat
if currentValue > updatedValue {
if currentValue > updatedValue || alwaysOneDirection {
offsetY = -floor(currentTextNode.bounds.height * 0.6)
} else {
offsetY = floor(currentTextNode.bounds.height * 0.6)

View File

@ -38,7 +38,7 @@ public func qrCodeCutout(size: Int, dimensions: CGSize, scale: CGFloat?) -> (Int
return (cutoutSize, cutoutRect, quadSize)
}
public func qrCode(string: String, color: UIColor, backgroundColor: UIColor? = nil, icon: QrCodeIcon, ecl: String = "M") -> Signal<(Int, (TransformImageArguments) -> DrawingContext?), NoError> {
public func qrCode(string: String, color: UIColor, backgroundColor: UIColor? = nil, icon: QrCodeIcon, ecl: String = "M", onlyMarkers: Bool = false) -> Signal<(Int, (TransformImageArguments) -> DrawingContext?), NoError> {
return Signal<(Data, Int, Int), NoError> { subscriber in
if let data = string.data(using: .isoLatin1, allowLossyConversion: false), let filter = CIFilter(name: "CIQRCodeGenerator") {
filter.setValue(data, forKey: "inputMessage")
@ -182,46 +182,48 @@ public func qrCode(string: String, color: UIColor, backgroundColor: UIColor? = n
}
}
for y in 0 ..< size {
for x in 0 ..< size {
if (y < markerSize + 1 && (x < markerSize + 1 || x > size - markerSize - 2)) || (y > size - markerSize - 2 && x < markerSize + 1) {
continue
}
var corners: UIRectCorner = []
if valueAt(x: x, y: y) {
corners = .allCorners
if valueAt(x: x, y: y - 1) {
corners.remove(.topLeft)
corners.remove(.topRight)
if !onlyMarkers {
for y in 0 ..< size {
for x in 0 ..< size {
if (y < markerSize + 1 && (x < markerSize + 1 || x > size - markerSize - 2)) || (y > size - markerSize - 2 && x < markerSize + 1) {
continue
}
if valueAt(x: x, y: y + 1) {
corners.remove(.bottomLeft)
corners.remove(.bottomRight)
var corners: UIRectCorner = []
if valueAt(x: x, y: y) {
corners = .allCorners
if valueAt(x: x, y: y - 1) {
corners.remove(.topLeft)
corners.remove(.topRight)
}
if valueAt(x: x, y: y + 1) {
corners.remove(.bottomLeft)
corners.remove(.bottomRight)
}
if valueAt(x: x - 1, y: y) {
corners.remove(.topLeft)
corners.remove(.bottomLeft)
}
if valueAt(x: x + 1, y: y) {
corners.remove(.topRight)
corners.remove(.bottomRight)
}
drawAt(x: x, y: y, fill: true, corners: corners)
} else {
if valueAt(x: x - 1, y: y - 1) && valueAt(x: x - 1, y: y) && valueAt(x: x, y: y - 1) {
corners.insert(.topLeft)
}
if valueAt(x: x + 1, y: y - 1) && valueAt(x: x + 1, y: y) && valueAt(x: x, y: y - 1) {
corners.insert(.topRight)
}
if valueAt(x: x - 1, y: y + 1) && valueAt(x: x - 1, y: y) && valueAt(x: x, y: y + 1) {
corners.insert(.bottomLeft)
}
if valueAt(x: x + 1, y: y + 1) && valueAt(x: x + 1, y: y) && valueAt(x: x, y: y + 1) {
corners.insert(.bottomRight)
}
drawAt(x: x, y: y, fill: false, corners: corners)
}
if valueAt(x: x - 1, y: y) {
corners.remove(.topLeft)
corners.remove(.bottomLeft)
}
if valueAt(x: x + 1, y: y) {
corners.remove(.topRight)
corners.remove(.bottomRight)
}
drawAt(x: x, y: y, fill: true, corners: corners)
} else {
if valueAt(x: x - 1, y: y - 1) && valueAt(x: x - 1, y: y) && valueAt(x: x, y: y - 1) {
corners.insert(.topLeft)
}
if valueAt(x: x + 1, y: y - 1) && valueAt(x: x + 1, y: y) && valueAt(x: x, y: y - 1) {
corners.insert(.topRight)
}
if valueAt(x: x - 1, y: y + 1) && valueAt(x: x - 1, y: y) && valueAt(x: x, y: y + 1) {
corners.insert(.bottomLeft)
}
if valueAt(x: x + 1, y: y + 1) && valueAt(x: x + 1, y: y) && valueAt(x: x, y: y + 1) {
corners.insert(.bottomRight)
}
drawAt(x: x, y: y, fill: false, corners: corners)
}
}
}

View File

@ -2,3 +2,34 @@ import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
func _internal_importContactToken(account: Account, token: String) -> Signal<EnginePeer?, NoError> {
return account.network.request(Api.functions.contacts.importContactToken(token: token))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.User?, NoError> in
return .single(nil)
}
|> map { result -> EnginePeer? in
return result.flatMap { EnginePeer(TelegramUser(user: $0)) }
}
}
public struct ExportedContactToken {
public let url: String
public let expires: Int32
}
func _internal_exportContactToken(account: Account) -> Signal<ExportedContactToken?, NoError> {
return account.network.request(Api.functions.contacts.exportContactToken())
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.ExportedContactToken?, NoError> in
return .single(nil)
}
|> map { result -> ExportedContactToken? in
if let result = result, case let .exportedContactToken(url, expires) = result {
return ExportedContactToken(url: url, expires: expires)
} else {
return nil
}
}
}

View File

@ -973,6 +973,14 @@ public extension TelegramEngine {
public func forumChannelTopicNotificationExceptions(id: EnginePeer.Id) -> Signal<[EngineMessageHistoryThread.NotificationException], NoError> {
return _internal_forumChannelTopicNotificationExceptions(account: self.account, id: id)
}
public func importContactToken(token: String) -> Signal<EnginePeer?, NoError> {
return _internal_importContactToken(account: self.account, token: token)
}
public func exportContactToken() -> Signal<ExportedContactToken?, NoError> {
return _internal_exportContactToken(account: self.account)
}
}
}

View File

@ -33,6 +33,7 @@ import TelegramUniversalVideoContent
import GalleryUI
import SaveToCameraRoll
import SegmentedControlNode
import AnimatedCountLabelNode
private func closeButtonImage(theme: PresentationTheme) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
@ -787,6 +788,8 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDeleg
private var containerLayout: (ContainerViewLayout, CGFloat)?
private let disposable = MetaDisposable()
private let contactDisposable = MetaDisposable()
private var currentContactToken: ExportedContactToken?
var present: ((ViewController) -> Void)?
var previewTheme: ((String?, Bool?, PresentationTheme) -> Void)?
@ -1154,6 +1157,43 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDeleg
}
self.ready.set(self.contentNode.isReady)
if case let .peer(_, _, temporary) = controller.subject, temporary {
self.contactDisposable.set(
(context.engine.peers.exportContactToken()
|> deliverOnMainQueue).start(next: { [weak self] token in
if let strongSelf = self {
strongSelf.currentContactToken = token
if let contentNode = strongSelf.contentNode as? QrContentNode, let token = token {
contentNode.setContactToken(token, animated: true)
}
}
})
)
if let contentNode = self.contentNode as? QrContentNode {
contentNode.requestNextToken = { [weak self] in
if let strongSelf = self {
strongSelf.contactDisposable.set(
(context.engine.peers.exportContactToken()
|> deliverOnMainQueue).start(next: { [weak self] token in
if let strongSelf = self {
strongSelf.currentContactToken = token
if let contentNode = strongSelf.contentNode as? QrContentNode, let token = token {
contentNode.setContactToken(token, animated: true)
}
}
})
)
}
}
}
}
}
deinit {
self.disposable.dispose()
self.contactDisposable.dispose()
}
private func enqueueTransition(_ transition: ThemeSettingsThemeItemNodeTransition) {
@ -1451,7 +1491,11 @@ private class QrContentNode: ASDisplayNode, ContentNode {
private var codeForegroundDimNode: ASDisplayNode
private let codeMaskNode: ASDisplayNode
private let codeTextNode: ImmediateTextNode
private let codeCountdownNode: ImmediateAnimatedCountLabelNode
private let codeImageNode: TransformImageNode
private let codeMarkersNode: TransformImageNode
private var codeSnapshotView: UIView?
private var codePlaceholderNode: AnimatedStickerNode
private let codeIconBackgroundNode: ASImageNode
private let codeStaticIconNode: ASImageNode?
private let codeAnimatedIconNode: AnimatedStickerNode?
@ -1471,7 +1515,10 @@ private class QrContentNode: ASDisplayNode, ContentNode {
}
private var timer: SwiftSignalKit.Timer?
private var setupTimestamp: Double
private var token: ExportedContactToken?
private var gettingNextToken = false
private var tokenUpdated = false
var requestNextToken: () -> Void = {}
init(context: AccountContext, peer: Peer, threadId: Int64?, isStatic: Bool = false, temporary: Bool) {
self.context = context
@ -1479,9 +1526,7 @@ private class QrContentNode: ASDisplayNode, ContentNode {
self.threadId = threadId
self.isStatic = isStatic
self.temporary = temporary
self.setupTimestamp = CACurrentMediaTime()
self.containerNode = ASDisplayNode()
self.wallpaperBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: false, useExperimentalImplementation: context.sharedContext.immediateExperimentalUISettings.experimentalBackground)
@ -1503,8 +1548,11 @@ private class QrContentNode: ASDisplayNode, ContentNode {
self.codeMaskNode = ASDisplayNode()
self.codeImageNode = TransformImageNode()
self.codeMarkersNode = TransformImageNode()
self.codeIconBackgroundNode = ASImageNode()
self.codePlaceholderNode = DefaultAnimatedStickerNodeImpl()
if isStatic {
let codeStaticIconNode = ASImageNode()
codeStaticIconNode.displaysAsynchronously = false
@ -1521,27 +1569,41 @@ private class QrContentNode: ASDisplayNode, ContentNode {
}
var codeText: String
if temporary {
codeText = "5:00"
if let addressName = peer.addressName, !addressName.isEmpty {
codeText = "@\(peer.addressName ?? "")".uppercased()
} else {
if let addressName = peer.addressName, !addressName.isEmpty {
codeText = "@\(peer.addressName ?? "")".uppercased()
} else {
codeText = peer.debugDisplayTitle.uppercased()
}
if let threadId = self.threadId, threadId != 0 {
codeText += "/\(threadId)"
}
codeText = peer.debugDisplayTitle.uppercased()
}
if let threadId = self.threadId, threadId != 0 {
codeText += "/\(threadId)"
}
let codeFont = Font.with(size: 23.0, design: .round, weight: .bold, traits: [.monospacedNumbers])
self.codeTextNode = ImmediateTextNode()
self.codeTextNode.displaysAsynchronously = false
self.codeTextNode.attributedText = NSAttributedString(string: codeText, font: Font.with(size: 23.0, design: .round, weight: .bold, traits: [.monospacedNumbers]), textColor: .black)
self.codeTextNode.attributedText = NSAttributedString(string: codeText, font: codeFont, textColor: .black)
self.codeTextNode.truncationMode = .byCharWrapping
self.codeTextNode.maximumNumberOfLines = 2
self.codeTextNode.textAlignment = .center
self.codeCountdownNode = ImmediateAnimatedCountLabelNode()
self.codeCountdownNode.alwaysOneDirection = true
if isStatic {
if temporary {
self.codeCountdownNode.isHidden = true
}
self.codeTextNode.setNeedsDisplayAtScale(3.0)
} else if temporary {
self.codeTextNode.isHidden = true
self.codeCountdownNode.segments = [
.number(Int(5), NSAttributedString(string: "5", font: codeFont, textColor: .black)),
.text(0, NSAttributedString(string: ":", font: codeFont, textColor: .black)),
.number(Int(0), NSAttributedString(string: "0", font: codeFont, textColor: .black)),
.number(Int(0), NSAttributedString(string: "0", font: codeFont, textColor: .black))
]
}
self.avatarNode = ImageNode()
@ -1560,8 +1622,13 @@ private class QrContentNode: ASDisplayNode, ContentNode {
self.codeForegroundNode.addSubnode(self.codeForegroundDimNode)
self.codeMaskNode.addSubnode(self.codeImageNode)
self.codeMaskNode.addSubnode(self.codeMarkersNode)
if temporary {
self.codeMaskNode.addSubnode(self.codePlaceholderNode)
}
self.codeMaskNode.addSubnode(self.codeIconBackgroundNode)
self.codeMaskNode.addSubnode(self.codeTextNode)
self.codeMaskNode.addSubnode(self.codeCountdownNode)
self.containerNode.addSubnode(self.avatarNode)
@ -1607,6 +1674,13 @@ private class QrContentNode: ASDisplayNode, ContentNode {
self?.tick()
}, queue: Queue.mainQueue())
self.timer?.start()
self.codePlaceholderNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "QrDataRain"), width: 512, height: 512, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.codePlaceholderNode.visibility = true
self.codeImageNode.alpha = 0.0
self.codeMarkersNode.setSignal(qrCode(string: "https://t.me/contact/000000:abcdef", color: .black, backgroundColor: nil, icon: .cutout, ecl: "Q", onlyMarkers: true) |> map { $0.1 }, attemptSynchronously: true)
}
}
@ -1621,14 +1695,85 @@ private class QrContentNode: ASDisplayNode, ContentNode {
}
private func tick() {
let timeout: Double = 5.0 * 60.0
let currentTime = CACurrentMediaTime()
let elapsed = max(0.0, timeout - (currentTime - self.setupTimestamp))
let currentTime = Int32(Date().timeIntervalSince1970)
let elapsed: Int32
if let token = self.token {
elapsed = token.expires - currentTime
} else {
elapsed = 300
}
let string = stringForDuration(max(0, elapsed))
let codeFont = Font.with(size: 23.0, design: .round, weight: .bold, traits: [.monospacedNumbers])
var segments: [AnimatedCountLabelNode.Segment] = []
for char in string {
if let intValue = Int(String(char)) {
segments.append(.number(intValue, NSAttributedString(string: String(char), font: codeFont, textColor: .black)))
} else {
segments.append(.text(0, NSAttributedString(string: String(char), font: codeFont, textColor: .black)))
}
}
self.codeCountdownNode.segments = segments
self.codeTextNode.attributedText = NSAttributedString(string: stringForDuration(Int32(elapsed)), font: Font.with(size: 23.0, design: .round, weight: .bold, traits: [.monospacedNumbers]), textColor: .black)
if let (size, topInset, bottomInset) = self.validLayout {
self.updateLayout(size: size, topInset: topInset, bottomInset: bottomInset, transition: .immediate)
}
if elapsed <= 1 {
if !self.gettingNextToken {
self.gettingNextToken = true
self.requestNextToken()
let codeSnapshotView = UIImageView(image: self.codeImageNode.image)
codeSnapshotView.frame = self.codeImageNode.frame
self.codeImageNode.view.superview?.addSubview(codeSnapshotView)
self.codeSnapshotView = codeSnapshotView
self.codeImageNode.isHidden = true
}
}
if self.tokenUpdated {
self.tokenUpdated = false
self.codeImageNode.isHidden = false
self.codeImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
if let codeSnapshotView = self.codeSnapshotView {
self.codeSnapshotView = nil
codeSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak codeSnapshotView] _ in
codeSnapshotView?.removeFromSuperview()
})
}
}
}
func setContactToken(_ token: ExportedContactToken, animated: Bool) {
self.token = token
if self.gettingNextToken {
self.gettingNextToken = false
self.tokenUpdated = true
}
self.codeImageNode.setSignal(qrCode(string: token.url, color: .black, backgroundColor: nil, icon: .cutout, ecl: "Q") |> beforeNext { [weak self] size, _ in
guard let strongSelf = self else {
return
}
strongSelf.qrCodeSize = size
if let (size, topInset, bottomInset) = strongSelf.validLayout {
strongSelf.updateLayout(size: size, topInset: topInset, bottomInset: bottomInset, transition: .immediate)
}
if strongSelf.codePlaceholderNode.visibility {
strongSelf.codeImageNode.alpha = 1.0
strongSelf.codeImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
strongSelf.codePlaceholderNode.alpha = 0.0
strongSelf.codePlaceholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] _ in
self?.codePlaceholderNode.visibility = false
})
}
} |> map { $0.1 }, attemptSynchronously: true)
}
func generateImage(completion: @escaping (UIImage?) -> Void) {
@ -1640,6 +1785,9 @@ private class QrContentNode: ASDisplayNode, ContentNode {
let scale: CGFloat = 3.0
let copyNode = QrContentNode(context: self.context, peer: self.peer, threadId: self.threadId, isStatic: true, temporary: false)
if let token = self.token {
copyNode.setContactToken(token, animated: false)
}
func prepare(view: UIView, scale: CGFloat) {
view.contentScaleFactor = scale
@ -1763,10 +1911,23 @@ private class QrContentNode: ASDisplayNode, ContentNode {
let imageFrame = CGRect(origin: CGPoint(x: floor((codeBackgroundFrame.width - imageSize.width) / 2.0), y: floor((codeBackgroundFrame.width - imageSize.height) / 2.0)), size: imageSize)
transition.updateFrame(node: self.codeImageNode, frame: imageFrame)
let makeMarkersLayout = self.codeMarkersNode.asyncLayout()
let markersApply = makeMarkersLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil, scale: self.isStatic ? 3.0 : nil ))
let _ = markersApply()
transition.updateFrame(node: self.codeMarkersNode, frame: imageFrame)
let codePlaceholderFrame = imageFrame.insetBy(dx: 10.0, dy: 10.0)
self.codePlaceholderNode.updateLayout(size: codePlaceholderFrame.size)
self.codePlaceholderNode.frame = codePlaceholderFrame
let codeTextSize = self.codeTextNode.updateLayout(CGSize(width: codeBackgroundFrame.width - floor(imageFrame.minX * 1.2), height: codeBackgroundFrame.height))
transition.updateFrame(node: self.codeTextNode, frame: CGRect(origin: CGPoint(x: floor((codeBackgroundFrame.width - codeTextSize.width) / 2.0), y: imageFrame.maxY + floor((codeBackgroundHeight - imageFrame.maxY - codeTextSize.height) / 2.0) - 5.0), size: codeTextSize))
let codeCountdownSize = self.codeCountdownNode.updateLayout(size: CGSize(width: codeBackgroundFrame.width - floor(imageFrame.minX * 1.2), height: codeBackgroundFrame.height), animated: true)
transition.updateFrame(node: self.codeCountdownNode, frame: CGRect(origin: CGPoint(x: floor((codeBackgroundFrame.width - codeCountdownSize.width) / 2.0), y: imageFrame.maxY + floor((codeBackgroundHeight - imageFrame.maxY - codeCountdownSize.height) / 2.0) - 5.0), size: codeCountdownSize))
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0), y: codeBackgroundFrame.minY - floor(avatarSize.height * 0.7)), size: avatarSize))
if let qrCodeSize = self.qrCodeSize {

View File

@ -556,6 +556,22 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
convertedUrl = "https://t.me/login/\(code)"
}
}
} else if parsedUrl.host == "contact" {
if let components = URLComponents(string: "/?" + query) {
var token: String?
if let queryItems = components.queryItems {
for queryItem in queryItems {
if let value = queryItem.value {
if queryItem.name == "token" {
token = value
}
}
}
}
if let token = token {
convertedUrl = "https://t.me/contact/\(token)"
}
}
} else if parsedUrl.host == "confirmphone" {
if let components = URLComponents(string: "/?" + query) {
var phone: String?

View File

@ -7024,11 +7024,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
threadId = Int64(message.messageId.id)
}
// var temporary = false
// if self.isSettings && self.data?.globalSettings?.privacySettings?.phoneDiscoveryEnabled == false {
// temporary = true
// }
controller.present(ChatQrCodeScreen(context: self.context, subject: .peer(peer: peer, threadId: threadId, temporary: false)), in: .window(.root))
var temporary = false
if self.isSettings && self.data?.globalSettings?.privacySettings?.phoneDiscoveryEnabled == false {
temporary = true
}
controller.present(ChatQrCodeScreen(context: self.context, subject: .peer(peer: peer, threadId: threadId, temporary: temporary)), in: .window(.root))
}
fileprivate func openSettings(section: PeerInfoSettingsSection) {

View File

@ -94,6 +94,7 @@ public enum ParsedInternalUrl {
case theme(String)
case phone(String, String?, String?)
case startAttach(String, String?, String?)
case contactToken(String)
}
private enum ParsedUrl {
@ -160,7 +161,7 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? {
if let _ = url {
return .internalInstantView(url: "https://t.me/\(query)")
}
} else if peerName == "login" {
} else if peerName == "contact" {
var code: String?
for queryItem in queryItems {
if let value = queryItem.value {
@ -290,6 +291,8 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? {
if let code = Int(pathComponents[1]) {
return .confirmationCode(code)
}
} else if peerName == "contact" {
return .contactToken(pathComponents[1])
} else if pathComponents[0] == "share" && pathComponents[1] == "url" {
if let queryItems = components.queryItems {
var url: String?
@ -655,6 +658,15 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl)
return .single(.inaccessiblePeer)
}
}
case let .contactToken(token):
return context.engine.peers.importContactToken(token: token)
|> mapToSignal { peer -> Signal<ResolvedUrl?, NoError> in
if let peer = peer {
return .single(.peer(peer._asPeer(), .info))
} else {
return .single(.peer(nil, .info))
}
}
case let .privateMessage(messageId, threadId, timecode):
return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId))
|> mapToSignal { peer -> Signal<ResolvedUrl?, NoError> in