Various Improvements

This commit is contained in:
Ilya Laktyushin 2021-12-23 13:18:00 +04:00
parent 888e5e1474
commit ed7cf17233
38 changed files with 1506 additions and 265 deletions

View File

@ -7168,12 +7168,6 @@ Sorry for the inconvenience.";
"Conversation.LargeEmojiEnable" = "Enable Large Emoji";
"Conversation.LargeEmojiEnabled" = "Large emoji enabled.";
"GroupInfo.QRCode.Info" = "Everyone on Telegram can scan this code to join this group.";
"ChannelInfo.QRCode.Info" = "Everyone on Telegram can scan this code to join this channel.";
"UserInfo.QRCode.InfoYou" = "Everyone on Telegram can scan this code to message you.";
"UserInfo.QRCode.InfoBot" = "Everyone on Telegram can scan this code to use this bot.";
"UserInfo.QRCode.InfoOther" = "Everyone on Telegram can scan this code to message %@.";
"PeerInfo.QRCode.Title" = "QR Code";
"ChatList.Archive" = "Archive";
@ -7190,8 +7184,17 @@ Sorry for the inconvenience.";
"Localization.ShowTranslate" = "Show Translate Button";
"Localization.ShowTranslateInfo" = "Show 'Translate' button in the message action menu.";
"Localization.DoNotTranslate" = "Do Not Translate";
"Localization.DoNotTranslateInfo" = "Do not show 'Translate' button in the message action menu for this language";
"Localization.DoNotTranslateManyInfo" = "Do not show 'Translate' button in the message action menu for this languages";
"Localization.DoNotTranslateInfo" = "Do not show 'Translate' button in the message action menu for this language.";
"Localization.DoNotTranslateManyInfo" = "Do not show 'Translate' button in the message action menu for this languages.";
"Localization.InterfaceLanguage" = "Interface Language";
"DoNotTranslate.Title" = "Do Not Translate";
"Contacts.ScanQrCode" = "Scan QR Code";
"Contacts.QrCode.MyCode" = "My QR Code";
"Contacts.QrCode.NoCodeFound" = "No valid QR code found in the image. Please try again.";
"AccessDenied.QrCode" = "Telegram needs access to your photo library to scan QR codes.\n\nPlease go to Settings > Privacy > Photos and set Telegram to ON.";
"AccessDenied.QrCamera" = "Telegram needs access to your camera to scan QR codes.\n\nPlease go to Settings > Privacy > Camera and set Telegram to ON.";
"Share.ShareToInstagramStories" = "Share to Instagram Stories";

View File

@ -630,6 +630,8 @@ public protocol SharedAccountContext: AnyObject {
func makeRecentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController & RecentSessionsController
func makeChatQrCodeScreen(context: AccountContext, peer: Peer) -> ViewController
func navigateToCurrentCall()
var hasOngoingCall: ValuePromise<Bool> { get }
var immediateHasOngoingCall: Bool { get }

View File

@ -30,6 +30,7 @@ swift_library(
"//submodules/UndoUI:UndoUI",
"//submodules/TextFormat:TextFormat",
"//submodules/Markdown:Markdown",
"//submodules/QrCodeUI:QrCodeUI",
],
visibility = [
"//visibility:public",

View File

@ -13,6 +13,7 @@ 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)])
@ -55,7 +56,7 @@ public final class AuthDataTransferSplashScreen: ViewController {
return
}
DeviceAccess.authorizeAccess(to: .camera(.video), presentationData: strongSelf.presentationData, present: { c, a in
DeviceAccess.authorizeAccess(to: .camera(.qrCode), presentationData: strongSelf.presentationData, present: { c, a in
guard let strongSelf = self else {
return
}
@ -70,7 +71,7 @@ public final class AuthDataTransferSplashScreen: ViewController {
guard granted else {
return
}
(strongSelf.navigationController as? NavigationController)?.replaceController(strongSelf, with: AuthTransferScanScreen(context: strongSelf.context, activeSessionsContext: strongSelf.activeSessionsContext), animated: true)
(strongSelf.navigationController as? NavigationController)?.replaceController(strongSelf, with: QrCodeScanScreen(context: strongSelf.context, subject: .authTransfer(activeSessionsContext: strongSelf.activeSessionsContext)), animated: true)
})
})

View File

@ -35,6 +35,7 @@ swift_library(
"//submodules/StickerResources:StickerResources",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/QrCodeUI:QrCodeUI",
],
visibility = [
"//visibility:public",

View File

@ -18,6 +18,7 @@ import TelegramPermissionsUI
import AppBundle
import StickerResources
import ContextUI
import QrCodeUI
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
if searchNode.expansionProgress > 0.0 && searchNode.expansionProgress < 1.0 {
@ -375,6 +376,43 @@ public class ContactsController: ViewController {
})
}
self.contactsNode.openQrScan = { [weak self] in
if let strongSelf = self {
let context = strongSelf.context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
DeviceAccess.authorizeAccess(to: .camera(.qrCode), presentationData: presentationData, present: { c, a in
c.presentationArguments = a
context.sharedContext.mainWindow?.present(c, on: .root)
}, openSettings: {
context.sharedContext.applicationBindings.openSettings()
}, { [weak self] granted in
guard let strongSelf = self else {
return
}
guard granted else {
strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true)
return
}
let controller = QrCodeScanScreen(context: strongSelf.context, subject: .peer)
controller.showMyCode = { [weak self, weak controller] in
if let strongSelf = self {
let _ = (strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.context.account.peerId)
|> deliverOnMainQueue).start(next: { [weak self, weak controller] peer in
if let strongSelf = self, let controller = controller {
controller.present(strongSelf.context.sharedContext.makeChatQrCodeScreen(context: strongSelf.context, peer: peer), in: .window(.root))
}
})
}
}
(strongSelf.navigationController as? NavigationController)?.pushViewController(controller, completion: {
if let strongSelf = self {
strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true)
}
})
})
}
}
self.contactsNode.contactListNode.openSortMenu = { [weak self] in
self?.presentSortMenu()
}

View File

@ -56,6 +56,7 @@ final class ContactsControllerNode: ASDisplayNode {
var requestAddContact: ((String) -> Void)?
var openPeopleNearby: (() -> Void)?
var openInvite: (() -> Void)?
var openQrScan: (() -> Void)?
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
@ -70,8 +71,12 @@ final class ContactsControllerNode: ASDisplayNode {
var addNearbyImpl: (() -> Void)?
var inviteImpl: (() -> Void)?
var qrScanImpl: (() -> Void)?
let options = [ContactListAdditionalOption(title: presentationData.strings.Contacts_AddPeopleNearby, icon: .generic(UIImage(bundleImageName: "Contact List/PeopleNearbyIcon")!), action: {
addNearbyImpl?()
}), ContactListAdditionalOption(title: presentationData.strings.Contacts_ScanQrCode, icon: .generic(UIImage(bundleImageName: "Settings/QrIcon")!), action: {
qrScanImpl?()
}), ContactListAdditionalOption(title: presentationData.strings.Contacts_InviteFriends, icon: .generic(UIImage(bundleImageName: "Contact List/AddMemberIcon")!), action: {
inviteImpl?()
})]
@ -128,6 +133,12 @@ final class ContactsControllerNode: ASDisplayNode {
}
}
qrScanImpl = { [weak self] in
if let strongSelf = self {
strongSelf.openQrScan?()
}
}
contextAction = { [weak self] peer, node, gesture in
self?.contextAction(peer: peer, node: node, gesture: gesture)
}

View File

@ -17,6 +17,7 @@ import AccountContext
public enum DeviceAccessCameraSubject {
case video
case videoCall
case qrCode
}
@ -30,6 +31,7 @@ public enum DeviceAccessMediaLibrarySubject {
case send
case save
case wallpaper
case qrCode
}
public enum DeviceAccessLocationSubject {
@ -269,6 +271,8 @@ public final class DeviceAccess {
text = presentationData.strings.AccessDenied_Camera
case .videoCall:
text = presentationData.strings.AccessDenied_VideoCallCamera
case .qrCode:
text = presentationData.strings.AccessDenied_QrCamera
}
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
openSettings()
@ -289,6 +293,8 @@ public final class DeviceAccess {
text = presentationData.strings.AccessDenied_Camera
case .videoCall:
text = presentationData.strings.AccessDenied_VideoCallCamera
case .qrCode:
text = presentationData.strings.AccessDenied_QrCamera
}
}
completion(false)
@ -345,6 +351,8 @@ public final class DeviceAccess {
text = presentationData.strings.AccessDenied_SaveMedia
case .wallpaper:
text = presentationData.strings.AccessDenied_Wallpapers
case .qrCode:
text = presentationData.strings.AccessDenied_QrCode
}
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
openSettings()

View File

@ -207,8 +207,11 @@ public final class TextNodeLayout: NSObject {
if line.isRTL {
hasRTL = true
}
spoilers.append(contentsOf: line.spoilers.map { ( $0.range, $0.frame.offsetBy(dx: line.frame.minX, dy: line.frame.minY)) })
spoilerWords.append(contentsOf: line.spoilerWords.map { ( $0.range, $0.frame.offsetBy(dx: line.frame.minX, dy: line.frame.minY)) })
let lineFrame = displayLineFrame(frame: line.frame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: size), cutout: cutout)
spoilers.append(contentsOf: line.spoilers.map { ( $0.range, $0.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY)) })
spoilerWords.append(contentsOf: line.spoilerWords.map { ( $0.range, $0.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY)) })
}
self.hasRTL = hasRTL
self.spoilers = spoilers
@ -1095,7 +1098,7 @@ public class TextNode: ASDisplayNode {
addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex, rightInset: truncated ? 12.0 : 0.0)
}
addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length - 1)
addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length)
} else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] {
let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil))
let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil))
@ -1176,7 +1179,7 @@ public class TextNode: ASDisplayNode {
addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex)
}
addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length - 1)
addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length)
} else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] {
let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil))
let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil))

View File

@ -540,7 +540,13 @@ public final class RecognizedTextSelectionNode: ASDisplayNode {
return self.view
}
if self.bounds.contains(point) {
return self.view
for recognition in self.recognitions {
let mappedRect = recognition.rect.convertTo(size: self.bounds.size)
if mappedRect.boundingFrame.insetBy(dx: -20.0, dy: -20.0).contains(point) {
return self.view
}
}
return nil
}
return nil
}

View File

@ -351,3 +351,33 @@ public func recognizedContent(postbox: Postbox, image: @escaping () -> UIImage?,
}
}
}
public func recognizeQRCode(in image: UIImage?) -> Signal<String?, NoError> {
if #available(iOS 11.0, *) {
guard let cgImage = image?.cgImage else {
return .complete()
}
return Signal { subscriber in
let barcodeRequest = VNDetectBarcodesRequest { request, error in
if let result = request.results?.first as? VNBarcodeObservation {
subscriber.putNext(result.payloadStringValue)
} else {
subscriber.putNext(nil)
}
subscriber.putCompletion()
}
barcodeRequest.preferBackgroundProcessing = true
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
try? handler.perform([barcodeRequest])
return ActionDisposable {
if #available(iOS 13.0, *) {
barcodeRequest.cancel()
}
}
}
} else {
return .single(nil)
}
}

View File

@ -201,8 +201,14 @@ public class InvisibleInkDustNode: ASDisplayNode {
}
let textLength = CGFloat((textNode.cachedLayout?.attributedString?.string ?? "").count)
let timeToRead = min(45.0, ceil(max(4.0, textLength * 0.04)))
var spoilersLength: Int = 0
if let spoilers = textNode.cachedLayout?.spoilers {
for spoiler in spoilers {
spoilersLength += spoiler.0.length
}
}
let timeToRead = min(45.0, ceil(max(4.0, Double(spoilersLength) * 0.04)))
Queue.mainQueue().after(timeToRead * UIView.animationDurationFactor()) {
self.isRevealed = false

View File

@ -339,13 +339,14 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
let hasTopStripe: Bool
let hasTopGroupInset: Bool
let noInsets: Bool
let noCorners: Bool
public let tag: ItemListItemTag?
let header: ListViewItemHeader?
let shimmering: ItemListPeerItemShimmering?
let displayDecorations: Bool
let disableInteractiveTransitionIfNecessary: Bool
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: EnginePeer, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: EnginePeer.Presence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, highlighted: Bool = false, selectable: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removePeer: @escaping (EnginePeer.Id) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, displayDecorations: Bool = true, disableInteractiveTransitionIfNecessary: Bool = false) {
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: EnginePeer, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: EnginePeer.Presence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, highlighted: Bool = false, selectable: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removePeer: @escaping (EnginePeer.Id) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, noCorners: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, displayDecorations: Bool = true, disableInteractiveTransitionIfNecessary: Bool = false) {
self.presentationData = presentationData
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
@ -373,6 +374,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
self.hasTopStripe = hasTopStripe
self.hasTopGroupInset = hasTopGroupInset
self.noInsets = noInsets
self.noCorners = noCorners
self.tag = tag
self.header = header
self.shimmering = shimmering
@ -999,7 +1001,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
strongSelf.addSubnode(strongSelf.maskNode)
}
let hasCorners = itemListHasRoundedBlockLayout(params) && !item.noInsets
let hasCorners = itemListHasRoundedBlockLayout(params) && !item.noCorners
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {

View File

@ -7,11 +7,11 @@ import TelegramPresentationData
import DeviceAccess
import AccountContext
public func legacyWallpaperPicker(context: AccountContext, presentationData: PresentationData) -> Signal<(LegacyComponentsContext) -> TGMediaAssetsController, Void> {
public func legacyWallpaperPicker(context: AccountContext, presentationData: PresentationData, subject: DeviceAccessMediaLibrarySubject = .wallpaper) -> Signal<(LegacyComponentsContext) -> TGMediaAssetsController, Void> {
return Signal { subscriber in
let intent = TGMediaAssetsControllerSetCustomWallpaperIntent
DeviceAccess.authorizeAccess(to: .mediaLibrary(.wallpaper), presentationData: presentationData, present: context.sharedContext.presentGlobalController, openSettings: context.sharedContext.applicationBindings.openSettings, { value in
DeviceAccess.authorizeAccess(to: .mediaLibrary(subject), presentationData: presentationData, present: context.sharedContext.presentGlobalController, openSettings: context.sharedContext.applicationBindings.openSettings, { value in
if !value {
subscriber.putError(Void())
return

View File

@ -22,6 +22,15 @@ swift_library(
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/GlassButtonNode:GlassButtonNode",
"//submodules/TextFormat:TextFormat",
"//submodules/Markdown:Markdown",
"//submodules/UndoUI:UndoUI",
"//submodules/Camera:Camera",
"//submodules/LegacyUI:LegacyUI",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/LegacyMediaPickerUI:LegacyMediaPickerUI",
"//submodules/ImageContentAnalysis:ImageContentAnalysis",
],
visibility = [
"//visibility:public",

View File

@ -0,0 +1,725 @@
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
import LegacyUI
import LegacyComponents
import LegacyMediaPickerUI
import ImageContentAnalysis
import PresentationDataUtils
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 QrCodeScanScreen: ViewController {
public enum Subject {
case authTransfer(activeSessionsContext: ActiveSessionsContext)
case peer
}
private let context: AccountContext
private let subject: QrCodeScanScreen.Subject
private var presentationData: PresentationData
private var codeDisposable: Disposable?
private var inForegroundDisposable: Disposable?
private let approveDisposable = MetaDisposable()
private var controllerNode: QrCodeScanScreenNode {
return self.displayNode as! QrCodeScanScreenNode
}
public var showMyCode: () -> Void = {}
private var codeResolved = false
public init(context: AccountContext, subject: QrCodeScanScreen.Subject) {
self.context = context
self.subject = subject
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! QrCodeScanScreenNode).updateInForeground(inForeground)
})
if case .peer = subject {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Contacts_QrCode_MyCode, style: .plain, target: self, action: #selector(self.myCodePressed))
} else {
#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 myCodePressed() {
self.showMyCode()
}
@objc private func testPressed() {
self.dismissWithSession(session: nil)
}
private func dismissWithSession(session: RecentAccountSession?) {
guard case let .authTransfer(activeSessionsContext) = self.subject else {
return
}
if let navigationController = navigationController as? NavigationController {
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 = QrCodeScanScreenNode(context: self.context, presentationData: self.presentationData, controller: self, subject: self.subject)
self.displayNodeDidLoad()
self.codeDisposable = ((self.displayNode as! QrCodeScanScreenNode).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, !strongSelf.codeResolved else {
return
}
guard let code = code else {
return
}
switch strongSelf.subject {
case let .authTransfer(activeSessionsContext):
if let url = URL(string: code), let parsedToken = parseAuthTransferUrl(url) {
strongSelf.approveDisposable.set((approveAuthTransferToken(account: strongSelf.context.account, token: parsedToken, activeSessionsContext: activeSessionsContext)
|> deliverOnMainQueue).start(next: { session in
guard let strongSelf = self else {
return
}
strongSelf.controllerNode.codeWithError = nil
if case let .authTransfer(activeSessionsContext) = strongSelf.subject {
Queue.mainQueue().after(1.5, {
activeSessionsContext.loadMore()
})
}
strongSelf.dismissWithSession(session: session)
}, error: { _ in
guard let strongSelf = self else {
return
}
strongSelf.controllerNode.codeWithError = code
strongSelf.controllerNode.updateFocusedRect(nil)
}))
}
case .peer:
if let _ = URL(string: code) {
strongSelf.controllerNode.resolveCode(code: code, completion: { [weak self] result in
if let strongSelf = self {
strongSelf.codeResolved = true
}
})
}
}
})
self.controllerNode.present = { [weak self] c in
self?.present(c, in: .window(.root))
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
(self.displayNode as! QrCodeScanScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
}
private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollViewDelegate {
private let context: AccountContext
private var presentationData: PresentationData
private weak var controller: QrCodeScanScreen?
private let subject: QrCodeScanScreen.Subject
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 galleryButtonNode: GlassButtonNode
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?
private let resolveDisposable = MetaDisposable()
fileprivate let focusedCode = ValuePromise<CameraCode?>(ignoreRepeated: true)
private var focusedRect: CGRect?
var present: (ViewController) -> Void = { _ in }
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, controller: QrCodeScanScreen, subject: QrCodeScanScreen.Subject) {
self.context = context
self.presentationData = presentationData
self.controller = controller
self.subject = subject
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.galleryButtonNode = GlassButtonNode(icon: UIImage(bundleImageName: "Wallet/CameraGalleryIcon")!, label: nil)
self.torchButtonNode = GlassButtonNode(icon: UIImage(bundleImageName: "Wallet/CameraFlashIcon")!, label: nil)
let title: String
var text: String
switch subject {
case .authTransfer:
title = presentationData.strings.AuthSessions_AddDevice_ScanTitle
text = presentationData.strings.AuthSessions_AddDevice_ScanInstallInfo
case .peer:
title = ""
text = ""
}
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.attributedText = NSAttributedString(string: title, 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)
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)
if case .peer = subject {
self.addSubnode(self.galleryButtonNode)
}
self.addSubnode(self.torchButtonNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.errorTextNode)
self.galleryButtonNode.addTarget(self, action: #selector(self.galleryPressed), forControlEvents: .touchUpInside)
self.torchButtonNode.addTarget(self, action: #selector(self.torchPressed), forControlEvents: .touchUpInside)
}
deinit {
self.codeDisposable.dispose()
self.torchDisposable?.dispose()
self.resolveDisposable.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: [CameraCode]
switch strongSelf.subject {
case .authTransfer:
filteredCodes = codes.filter { $0.message.hasPrefix("tg://") }
case .peer:
filteredCodes = codes.filter { $0.message.hasPrefix("https://t.me/") || $0.message.hasPrefix("t.me/") }
}
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
var galleryFrame = torchFrame
if case .peer = self.subject {
galleryFrame.origin.x -= buttonSize.width
torchFrame.origin.x += buttonSize.width
}
transition.updateFrame(node: self.galleryButtonNode, frame: galleryFrame)
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.galleryButtonNode, 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 galleryPressed() {
let context = self.context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let presentError = { [weak self] in
let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.Contacts_QrCode_NoCodeFound, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
self?.present(alertController)
}
let _ = legacyWallpaperPicker(context: context, presentationData: presentationData, subject: .qrCode).start(next: { [weak self] generator in
let legacyController = LegacyController(presentation: .modal(animateIn: true), theme: presentationData.theme)
legacyController.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style
let controller = generator(legacyController.context)
legacyController.bind(controller: controller)
legacyController.deferScreenEdgeGestures = [.top]
controller.selectionBlock = { [weak legacyController] asset, _ in
if let asset = asset {
TGMediaAssetImageSignals.image(for: asset, imageType: TGMediaAssetImageTypeScreen, size: CGSize(width: 1280.0, height: 1280.0)).start(next: { image in
if let image = image as? UIImage {
let _ = (recognizeQRCode(in: image)
|> deliverOnMainQueue).start(next: { [weak self] result in
if let result = result, let strongSelf = self {
strongSelf.resolveCode(code: result, completion: { result in
if result {
} else {
presentError()
}
})
} else {
presentError()
}
})
} else {
presentError()
}
}, error: { _ in
presentError()
}, completed: {
})
legacyController?.dismiss()
}
}
controller.dismissalBlock = { [weak legacyController] in
if let legacyController = legacyController {
legacyController.dismiss()
}
}
self?.present(legacyController)
})
}
@objc private func torchPressed() {
self.torchButtonNode.isSelected = !self.torchButtonNode.isSelected
self.camera.setTorchActive(self.torchButtonNode.isSelected)
}
fileprivate func resolveCode(code: String, completion: @escaping (Bool) -> Void) {
self.resolveDisposable.set((self.context.sharedContext.resolveUrl(context: self.context, peerId: nil, url: code, skipUrlAuth: false)
|> deliverOnMainQueue).start(next: { [weak self] result in
if let strongSelf = self {
completion(strongSelf.openResolved(result))
}
}))
}
private func openResolved(_ result: ResolvedUrl) -> Bool {
switch result {
case .peer, .stickerPack, .join, .wallpaper, .theme:
break
default:
return false
}
guard let navigationController = self.controller?.navigationController as? NavigationController else {
return false
}
self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: .generic, navigationController: navigationController, openPeer: { [weak self] peerId, navigation in
guard let strongSelf = self else {
return
}
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), subject: nil, keepStack: .always, peekData: nil, completion: { [weak navigationController] _ in
if let navigationController = navigationController {
var viewControllers = navigationController.viewControllers
viewControllers = viewControllers.filter { controller in
if controller is QrCodeScanScreen {
return false
}
return true
}
navigationController.setViewControllers(viewControllers, animated: false)
}
}))
}, sendFile: nil,
sendSticker: { _, _, _ in
return false
}, requestMessageActionUrlAuth: nil,
joinVoiceChat: { peerId, invite, call in
}, present: { [weak self] c, a in
self?.controller?.present(c, in: .window(.root), with: a)
}, dismissInput: { [weak self] in
self?.view.endEditing(true)
}, contentContext: nil)
return true
}
}

View File

@ -230,24 +230,12 @@ public final class QrCodeScreen: ViewController {
let title: String
let text: String
switch subject {
case let .peer(peer):
title = self.presentationData.strings.PeerInfo_QRCode_Title
if case let .user(user) = peer {
if user.id == context.account.peerId {
text = self.presentationData.strings.UserInfo_QRCode_InfoYou
} else if user.botInfo != nil {
text = self.presentationData.strings.UserInfo_QRCode_InfoBot
} else {
text = self.presentationData.strings.UserInfo_QRCode_InfoOther(peer.compactDisplayTitle).string
}
} else if case let .channel(channel) = peer, case .broadcast = channel.info {
text = self.presentationData.strings.GroupInfo_QRCode_Info
} else {
text = self.presentationData.strings.ChannelInfo_QRCode_Info
}
case let .invite(_, isGroup):
title = self.presentationData.strings.InviteLink_QRCode_Title
text = isGroup ? self.presentationData.strings.InviteLink_QRCode_Info : self.presentationData.strings.InviteLink_QRCode_InfoChannel
default:
title = ""
text = ""
}
self.titleNode = ASTextNode()

View File

@ -93,6 +93,7 @@ swift_library(
"//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode",
"//submodules/WebPBinding:WebPBinding",
"//submodules/Translate:Translate",
"//submodules/QrCodeUI:QrCodeUI",
],
visibility = [
"//visibility:public",

View File

@ -12,6 +12,7 @@ import AccountContext
import AuthTransferUI
import ItemListPeerActionItem
import DeviceAccess
import QrCodeUI
private final class RecentSessionsControllerArguments {
let context: AccountContext
@ -749,7 +750,7 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, addDevice: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
DeviceAccess.authorizeAccess(to: .camera(.video), presentationData: presentationData, present: { c, a in
DeviceAccess.authorizeAccess(to: .camera(.qrCode), presentationData: presentationData, present: { c, a in
c.presentationArguments = a
context.sharedContext.mainWindow?.present(c, on: .root)
}, openSettings: {
@ -758,7 +759,7 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont
guard granted else {
return
}
pushControllerImpl?(AuthTransferScanScreen(context: context, activeSessionsContext: activeSessionsContext))
pushControllerImpl?(QrCodeScanScreen(context: context, subject: .authTransfer(activeSessionsContext: activeSessionsContext)))
})
}, openOtherAppsUrl: {
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://telegram.org/apps", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {})

View File

@ -1080,41 +1080,50 @@ final class MessageStoryRenderer {
}
}
private class ShareToInstagramActivity: UIActivity {
public class ShareToInstagramActivity: UIActivity {
private let context: AccountContext
private var activityItems = [Any]()
private var action: ([Any]) -> Void
init(action: @escaping ([Any]) -> Void) {
self.action = action
public init(context: AccountContext) {
self.context = context
super.init()
}
override var activityTitle: String? {
return "Share to Instagram Stories"
public override var activityTitle: String? {
return self.context.sharedContext.currentPresentationData.with { $0 }.strings.Share_ShareToInstagramStories
}
override var activityImage: UIImage? {
return nil
public override var activityImage: UIImage? {
return UIImage(bundleImageName: "Share/Instagram")
}
override var activityType: UIActivity.ActivityType? {
public override var activityType: UIActivity.ActivityType? {
return UIActivity.ActivityType(rawValue: "org.telegram.Telegram.ShareToInstagram")
}
override class var activityCategory: UIActivity.Category {
public override class var activityCategory: UIActivity.Category {
return .action
}
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
return true
public override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
return self.context.sharedContext.applicationBindings.canOpenUrl("instagram-stories://")
}
override func prepare(withActivityItems activityItems: [Any]) {
public override func prepare(withActivityItems activityItems: [Any]) {
self.activityItems = activityItems
}
override func perform() {
self.action(self.activityItems)
public override func perform() {
if let url = self.activityItems.first as? URL, let data = try? Data(contentsOf: url) {
let pasteboardItems: [[String: Any]] = [["com.instagram.sharedSticker.backgroundImage": data]]
if #available(iOS 10.0, *) {
UIPasteboard.general.setItems(pasteboardItems, options: [.expirationDate: Date().addingTimeInterval(5 * 60)])
} else {
UIPasteboard.general.items = pasteboardItems
}
context.sharedContext.applicationBindings.openUrl("instagram-stories://share")
}
activityDidFinish(true)
}
}

View File

@ -228,6 +228,8 @@ public final class PermissionController: ViewController {
self.displayNode = PermissionControllerNode(context: self.context, splitTest: self.splitTest)
self.displayNodeDidLoad()
self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
self.controllerNode.allow = { [weak self] in
self?.allow?()
}

View File

@ -79,7 +79,7 @@ public func customizeDefaultDarkPresentationTheme(theme: PresentationTheme, edit
} else {
badgeFillColor = UIColor(rgb: 0xeb5545)
badgeTextColor = UIColor(rgb: 0xffffff)
if initialAccentColor.lightness > 0.7 {
if initialAccentColor.lightness > 0.735 {
secondaryBadgeTextColor = UIColor(rgb: 0x000000)
} else {
secondaryBadgeTextColor = UIColor(rgb: 0xffffff)
@ -153,7 +153,7 @@ public func customizeDefaultDarkPresentationTheme(theme: PresentationTheme, edit
outgoingBubbleFillColors = bubbleColors.map(UIColor.init(rgb:))
let lightnessColor = topBubbleColor.mixedWith(bottomBubbleColor, alpha: 0.5)
if lightnessColor.lightness > 0.7 {
if lightnessColor.lightness > 0.735 {
outgoingPrimaryTextColor = UIColor(rgb: 0x000000)
outgoingSecondaryTextColor = UIColor(rgb: 0x000000, alpha: 0.5)
outgoingLinkTextColor = UIColor(rgb: 0x000000)

View File

@ -309,7 +309,6 @@ public func currentPresentationDataAndSettings(accountManager: AccountManager<Te
let effectiveColors = themeSettings.themeSpecificAccentColors[effectiveTheme.index]
let theme = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: effectiveTheme, baseTheme: preferredBaseTheme, accentColor: effectiveColors?.color, bubbleColors: effectiveColors?.customBubbleColors ?? [], baseColor: effectiveColors?.baseColor) ?? defaultPresentationTheme
let effectiveChatWallpaper: TelegramWallpaper = (themeSettings.themeSpecificChatWallpapers[coloredThemeIndex(reference: effectiveTheme, accentColor: effectiveColors)] ?? themeSettings.themeSpecificChatWallpapers[effectiveTheme.index]) ?? theme.chat.defaultWallpaper
let dateTimeFormat = currentDateTimeFormat()

View File

@ -1,9 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "instagram_60.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,151 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 10.500000 10.500000 cm
0.000000 0.000000 0.000000 scn
17.432882 39.000000 m
17.500000 39.000000 l
21.500000 39.000000 l
21.567118 39.000000 l
24.310560 39.000008 26.461430 39.000015 28.190128 38.858776 c
29.950411 38.714954 31.404631 38.417244 32.720890 37.746574 c
34.884754 36.644032 36.644032 34.884754 37.746574 32.720890 c
38.417244 31.404629 38.714954 29.950411 38.858776 28.190128 c
39.000015 26.461479 39.000008 24.310677 39.000000 21.567352 c
39.000000 21.567255 l
39.000000 21.567158 l
39.000000 21.500000 l
39.000000 17.500000 l
39.000000 17.432842 l
39.000000 17.432745 l
39.000000 17.432648 l
39.000008 14.689320 39.000015 12.538519 38.858776 10.809872 c
38.714954 9.049589 38.417244 7.595369 37.746574 6.279110 c
36.644032 4.115246 34.884754 2.355968 32.720890 1.253426 c
31.404631 0.582756 29.950411 0.285046 28.190128 0.141224 c
26.461481 -0.000015 24.310678 -0.000008 21.567352 0.000000 c
21.567255 0.000000 l
21.567158 0.000000 l
21.500000 0.000000 l
17.500000 0.000000 l
17.432840 0.000000 l
17.432743 0.000000 l
17.432646 0.000000 l
14.689321 -0.000008 12.538522 -0.000015 10.809872 0.141224 c
9.049589 0.285046 7.595370 0.582756 6.279109 1.253426 c
4.115245 2.355968 2.355969 4.115246 1.253425 6.279110 c
0.582757 7.595369 0.285045 9.049589 0.141224 10.809872 c
-0.000016 12.538570 -0.000009 14.689442 0.000000 17.432882 c
0.000000 17.500000 l
0.000000 21.500000 l
0.000000 21.567120 l
-0.000009 24.310558 -0.000016 26.461430 0.141224 28.190128 c
0.285045 29.950411 0.582757 31.404629 1.253425 32.720890 c
2.355969 34.884754 4.115245 36.644032 6.279109 37.746574 c
7.595370 38.417244 9.049589 38.714954 10.809872 38.858776 c
12.538570 39.000015 14.689443 39.000008 17.432882 39.000000 c
h
11.054168 35.868740 m
9.479408 35.740078 8.463938 35.492821 7.641081 35.073555 c
6.041704 34.258633 4.741369 32.958298 3.926445 31.358919 c
3.507179 30.536062 3.259924 29.520592 3.131261 27.945833 c
3.001167 26.353561 3.000000 24.325014 3.000000 21.500000 c
3.000000 17.500000 l
3.000000 14.674986 3.001167 12.646439 3.131261 11.054167 c
3.259924 9.479408 3.507179 8.463938 3.926445 7.641081 c
4.741369 6.041702 6.041704 4.741367 7.641081 3.926445 c
8.463938 3.507179 9.479408 3.259922 11.054167 3.131260 c
12.646438 3.001167 14.674986 3.000000 17.500000 3.000000 c
21.500000 3.000000 l
24.325014 3.000000 26.353561 3.001167 27.945833 3.131260 c
29.520592 3.259922 30.536062 3.507179 31.358919 3.926445 c
32.958298 4.741367 34.258633 6.041702 35.073555 7.641081 c
35.492821 8.463938 35.740078 9.479408 35.868740 11.054167 c
35.998833 12.646437 36.000000 14.674986 36.000000 17.500000 c
36.000000 21.500000 l
36.000000 24.325014 35.998833 26.353561 35.868740 27.945831 c
35.740078 29.520592 35.492821 30.536062 35.073555 31.358919 c
34.258633 32.958298 32.958298 34.258633 31.358919 35.073555 c
30.536062 35.492821 29.520592 35.740078 27.945833 35.868740 c
26.353563 35.998833 24.325014 36.000000 21.500000 36.000000 c
17.500000 36.000000 l
14.674986 36.000000 12.646438 35.998833 11.054168 35.868740 c
h
32.100098 30.099976 m
32.100098 28.995407 31.204668 28.099976 30.100098 28.099976 c
28.995527 28.099976 28.100098 28.995407 28.100098 30.099976 c
28.100098 31.204544 28.995527 32.099976 30.100098 32.099976 c
31.204668 32.099976 32.100098 31.204544 32.100098 30.099976 c
h
12.000000 19.500000 m
12.000000 23.642136 15.357864 27.000000 19.500000 27.000000 c
23.642136 27.000000 27.000000 23.642136 27.000000 19.500000 c
27.000000 15.357864 23.642136 12.000000 19.500000 12.000000 c
15.357864 12.000000 12.000000 15.357864 12.000000 19.500000 c
h
19.500000 30.000000 m
13.701010 30.000000 9.000000 25.298990 9.000000 19.500000 c
9.000000 13.701010 13.701010 9.000000 19.500000 9.000000 c
25.298990 9.000000 30.000000 13.701010 30.000000 19.500000 c
30.000000 25.298990 25.298990 30.000000 19.500000 30.000000 c
h
f*
n
Q
endstream
endobj
3 0 obj
3898
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 60.000000 60.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000003988 00000 n
0000004011 00000 n
0000004184 00000 n
0000004258 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
4317
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_qrlogo.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -135,7 +135,7 @@ func chatHistoryEntriesForView(
}
if presentationData.largeEmoji, message.media.isEmpty {
if stickersEnabled && message.text.count == 1, let _ = associatedData.animatedEmojiStickers[message.text.basicEmoji.0] {
if stickersEnabled && message.text.count == 1, let _ = associatedData.animatedEmojiStickers[message.text.basicEmoji.0], (message.textEntitiesAttribute?.entities.isEmpty ?? true) {
contentTypeHint = .animatedEmoji
} else if message.text.count < 10 && messageIsElligibleForLargeEmoji(message) {
contentTypeHint = .largeEmoji

View File

@ -23,6 +23,7 @@ import ShimmerEffect
import WallpaperBackgroundNode
import QrCode
import AvatarNode
import ShareController
private func closeButtonImage(theme: PresentationTheme) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
@ -554,7 +555,6 @@ final class ChatQrCodeScreen: ViewController {
private var animatedIn = false
private let context: AccountContext
private let animatedEmojiStickers: [String: [StickerPackItem]]
private let peer: Peer
private var presentationData: PresentationData
@ -563,10 +563,9 @@ final class ChatQrCodeScreen: ViewController {
var dismissed: (() -> Void)?
init(context: AccountContext, animatedEmojiStickers: [String: [StickerPackItem]], peer: Peer) {
init(context: AccountContext, peer: Peer) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.animatedEmojiStickers = animatedEmojiStickers
self.peer = peer
super.init(navigationBarPresentationData: nil)
@ -591,6 +590,8 @@ final class ChatQrCodeScreen: ViewController {
})
self.statusBar.statusBarStyle = .Ignore
self.ready.set(self.controllerNode.ready.get())
}
required init(coder aDecoder: NSCoder) {
@ -602,7 +603,7 @@ final class ChatQrCodeScreen: ViewController {
}
override public func loadDisplayNode() {
self.displayNode = ChatQrCodeScreenNode(context: self.context, presentationData: self.presentationData, controller: self, animatedEmojiStickers: self.animatedEmojiStickers, peer: self.peer)
self.displayNode = ChatQrCodeScreenNode(context: self.context, presentationData: self.presentationData, controller: self, peer: self.peer)
self.controllerNode.previewTheme = { [weak self] _, _, theme in
self?.presentationThemePromise.set(.single(theme))
}
@ -659,10 +660,6 @@ final class ChatQrCodeScreen: ViewController {
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
func dimTapped() {
self.controllerNode.dimTapped()
}
}
private func iconColors(theme: PresentationTheme) -> [String: UIColor] {
@ -689,20 +686,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDeleg
private var presentationData: PresentationData
private weak var controller: ChatQrCodeScreen?
private let dimNode: ASDisplayNode
private let containerNode: ASDisplayNode
private let wallpaperBackgroundNode: WallpaperBackgroundNode
private let codeBackgroundNode: ASDisplayNode
private let codeForegroundNode: ASDisplayNode
private var codeForegroundContentNode: ASDisplayNode?
private var codeForegroundDimNode: ASDisplayNode
private let codeMaskNode: ASDisplayNode
private let codeTextNode: ImmediateTextNode
private let codeImageNode: TransformImageNode
private let codeIconBackgroundNode: ASImageNode
private let codeIconNode: AnimatedStickerNode
private let avatarNode: ImageNode
private var qrCodeSize: Int?
private let contentNode: QrContentNode
private let wrappingScrollNode: ASScrollNode
private let contentContainerNode: ASDisplayNode
@ -723,14 +707,17 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDeleg
private var initialized = false
private var themes: [TelegramTheme] = []
let ready = Promise<Bool>()
private let peer: Peer
private var selectedEmoticon: String? {
private var initiallySelectedEmoticon: String?
private var selectedEmoticon: String? = nil {
didSet {
self.selectedEmoticonPromise.set(self.selectedEmoticon)
}
}
private var selectedEmoticonPromise: ValuePromise<String?>
private var selectedEmoticonPromise = ValuePromise<String?>(nil)
private var isDarkAppearancePromise: ValuePromise<Bool>
private var isDarkAppearance: Bool = false {
@ -749,26 +736,19 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDeleg
var dismiss: (() -> Void)?
var cancel: (() -> Void)?
init(context: AccountContext, presentationData: PresentationData, controller: ChatQrCodeScreen, animatedEmojiStickers: [String: [StickerPackItem]], peer: Peer) {
init(context: AccountContext, presentationData: PresentationData, controller: ChatQrCodeScreen, peer: Peer) {
self.context = context
self.controller = controller
self.peer = peer
self.selectedEmoticon = defaultEmoticon
self.selectedEmoticonPromise = ValuePromise(self.selectedEmoticon)
self.presentationData = presentationData
self.wrappingScrollNode = ASScrollNode()
self.wrappingScrollNode.view.alwaysBounceVertical = true
self.wrappingScrollNode.view.delaysContentTouches = false
self.wrappingScrollNode.view.canCancelContentTouches = true
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = .clear
self.containerNode = ASDisplayNode()
self.wallpaperBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: false, useExperimentalImplementation: self.context.sharedContext.immediateExperimentalUISettings.experimentalBackground)
self.contentNode = QrContentNode(context: context, peer: peer, isStatic: false)
self.contentContainerNode = ASDisplayNode()
self.contentContainerNode.isOpaque = false
@ -812,62 +792,14 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDeleg
self.listNode = ListView()
self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
self.codeBackgroundNode = ASDisplayNode()
self.codeBackgroundNode.backgroundColor = .white
self.codeBackgroundNode.cornerRadius = 42.0
if #available(iOS 13.0, *) {
self.codeBackgroundNode.layer.cornerCurve = .continuous
}
self.codeForegroundNode = ASDisplayNode()
self.codeForegroundNode.backgroundColor = .black
self.codeForegroundDimNode = ASDisplayNode()
self.codeForegroundDimNode.alpha = 0.3
self.codeForegroundDimNode.backgroundColor = .black
self.codeMaskNode = ASDisplayNode()
self.codeImageNode = TransformImageNode()
self.codeIconBackgroundNode = ASImageNode()
self.codeIconNode = AnimatedStickerNode()
self.codeIconNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "PlaneLogoPlain"), width: 240, height: 240, mode: .direct(cachePathPrefix: nil))
self.codeIconNode.visibility = true
self.codeTextNode = ImmediateTextNode()
self.codeTextNode.attributedText = NSAttributedString(string: "@\(peer.addressName ?? "")".uppercased(), font: Font.with(size: 24.0, design: .round, weight: .bold, traits: []), textColor: .black)
self.avatarNode = ImageNode()
self.avatarNode.displaysAsynchronously = false
self.avatarNode.setSignal(peerAvatarCompleteImage(account: self.context.account, peer: EnginePeer(peer), size: CGSize(width: 180.0, height: 180.0), font: avatarPlaceholderFont(size: 78.0), fullSize: true))
super.init()
self.backgroundColor = nil
self.isOpaque = false
self.addSubnode(self.dimNode)
self.wrappingScrollNode.view.delegate = self
self.addSubnode(self.wrappingScrollNode)
self.wrappingScrollNode.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.wallpaperBackgroundNode)
self.containerNode.addSubnode(self.codeBackgroundNode)
self.containerNode.addSubnode(self.codeForegroundNode)
self.codeForegroundNode.addSubnode(self.codeForegroundDimNode)
self.codeMaskNode.addSubnode(self.codeImageNode)
self.codeMaskNode.addSubnode(self.codeIconBackgroundNode)
self.codeMaskNode.addSubnode(self.codeTextNode)
self.containerNode.addSubnode(self.avatarNode)
self.wrappingScrollNode.addSubnode(self.codeIconNode)
self.wrappingScrollNode.addSubnode(self.contentNode)
self.wrappingScrollNode.addSubnode(self.backgroundNode)
self.wrappingScrollNode.addSubnode(self.contentContainerNode)
@ -889,20 +821,102 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDeleg
self.doneButton.pressed = { [weak self] in
if let strongSelf = self {
strongSelf.doneButton.isUserInteractionEnabled = false
strongSelf.completion?(strongSelf.selectedEmoticon)
strongSelf.contentNode.generateImage { [weak self] image in
if let strongSelf = self, let image = image, let jpgData = image.jpegData(compressionQuality: 0.9) {
let tempFilePath = NSTemporaryDirectory() + "t_me-\(peer.addressName ?? "").jpg"
try? FileManager.default.removeItem(atPath: tempFilePath)
let tempFileUrl = URL(fileURLWithPath: tempFilePath)
try? jpgData.write(to: tempFileUrl)
let activityController = UIActivityViewController(activityItems: [tempFileUrl], applicationActivities: [ShareToInstagramActivity(context: strongSelf.context)])
activityController.completionWithItemsHandler = { [weak self] _, finished, _, _ in
if let strongSelf = self {
if finished {
strongSelf.completion?(strongSelf.selectedEmoticon)
} else {
strongSelf.doneButton.isUserInteractionEnabled = true
}
}
}
if let window = strongSelf.view.window {
activityController.popoverPresentationController?.sourceView = window
activityController.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0))
}
context.sharedContext.applicationBindings.presentNativeController(activityController)
}
}
}
}
self.disposable.set(combineLatest(queue: Queue.mainQueue(), self.context.engine.themes.getChatThemes(accountManager: self.context.sharedContext.accountManager), self.selectedEmoticonPromise.get(), self.isDarkAppearancePromise.get()).start(next: { [weak self] themes, selectedEmoticon, isDarkAppearance in
let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false)
|> map { animatedEmoji -> [String: [StickerPackItem]] in
var animatedEmojiStickers: [String: [StickerPackItem]] = [:]
switch animatedEmoji {
case let .result(_, items, _):
for item in items {
if let emoji = item.getStringRepresentationsOfIndexKeys().first {
animatedEmojiStickers[emoji.basicEmoji.0] = [item]
let strippedEmoji = emoji.basicEmoji.0.strippedEmoji
if animatedEmojiStickers[strippedEmoji] == nil {
animatedEmojiStickers[strippedEmoji] = [item]
}
}
}
default:
break
}
return animatedEmojiStickers
}
let initiallySelectedEmoticon: Signal<String, NoError>
if self.peer.id == self.context.account.peerId {
initiallySelectedEmoticon = self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings])
|> take(1)
|> map { sharedData -> String in
let themeSettings: PresentationThemeSettings
if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) {
themeSettings = current
} else {
themeSettings = PresentationThemeSettings.defaultSettings
}
return themeSettings.theme.emoticon ?? defaultEmoticon
}
} else {
initiallySelectedEmoticon = self.context.account.postbox.transaction { transaction in
return transaction.getPeerCachedData(peerId: peer.id)
}
|> take(1)
|> map { cachedData -> String in
if let cachedData = cachedData as? CachedUserData {
return cachedData.themeEmoticon ?? defaultEmoticon
} else if let cachedData = cachedData as? CachedGroupData {
return cachedData.themeEmoticon ?? defaultEmoticon
} else if let cachedData = cachedData as? CachedChannelData {
return cachedData.themeEmoticon ?? defaultEmoticon
} else {
return defaultEmoticon
}
}
}
self.disposable.set(combineLatest(queue: Queue.mainQueue(), animatedEmojiStickers, initiallySelectedEmoticon, self.context.engine.themes.getChatThemes(accountManager: self.context.sharedContext.accountManager), self.selectedEmoticonPromise.get(), self.isDarkAppearancePromise.get()).start(next: { [weak self] animatedEmojiStickers, initiallySelectedEmoticon, themes, selectedEmoticon, isDarkAppearance in
guard let strongSelf = self else {
return
}
var selectedEmoticon = selectedEmoticon
if strongSelf.initiallySelectedEmoticon == nil {
strongSelf.initiallySelectedEmoticon = initiallySelectedEmoticon
strongSelf.selectedEmoticon = initiallySelectedEmoticon
selectedEmoticon = initiallySelectedEmoticon
}
let isFirstTime = strongSelf.entries == nil
let presentationData = strongSelf.presentationData
var entries: [ThemeSettingsThemeEntry] = []
entries.append(ThemeSettingsThemeEntry(index: 0, emoticon: defaultEmoticon, emojiFile: animatedEmojiStickers[defaultEmoticon]?.first?.file, themeReference: .builtin(.dayClassic), nightMode: isDarkAppearance, selected: selectedEmoticon == defaultEmoticon, theme: presentationData.theme, strings: presentationData.strings, wallpaper: nil))
entries.append(ThemeSettingsThemeEntry(index: 0, emoticon: defaultEmoticon, emojiFile: animatedEmojiStickers[defaultEmoticon]?.first?.file, themeReference: .builtin(isDarkAppearance ? .night : .dayClassic), nightMode: isDarkAppearance, selected: selectedEmoticon == defaultEmoticon, theme: presentationData.theme, strings: presentationData.strings, wallpaper: nil))
for theme in themes {
guard let emoticon = theme.emoticon else {
continue
@ -910,13 +924,14 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDeleg
entries.append(ThemeSettingsThemeEntry(index: entries.count, emoticon: emoticon, emojiFile: animatedEmojiStickers[emoticon]?.first?.file, themeReference: .cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: nil)), nightMode: isDarkAppearance, selected: selectedEmoticon == theme.emoticon, theme: presentationData.theme, strings: presentationData.strings, wallpaper: nil))
}
let wallpaper: TelegramWallpaper
if selectedEmoticon == defaultEmoticon {
let presentationTheme = makeDefaultPresentationTheme(reference: isDarkAppearance ? .night : .dayClassic, serviceBackgroundColor: nil)
strongSelf.wallpaperBackgroundNode.update(wallpaper: presentationTheme.chat.defaultWallpaper)
} else if let theme = themes.first(where: { $0.emoticon == selectedEmoticon }) {
if let presentationTheme = makePresentationTheme(cloudTheme: theme, dark: isDarkAppearance) {
strongSelf.wallpaperBackgroundNode.update(wallpaper: presentationTheme.chat.defaultWallpaper)
}
wallpaper = presentationTheme.chat.defaultWallpaper
} else if let theme = themes.first(where: { $0.emoticon == selectedEmoticon }), let presentationTheme = makePresentationTheme(cloudTheme: theme, dark: isDarkAppearance) {
wallpaper = presentationTheme.chat.defaultWallpaper
} else {
wallpaper = .color(0x000000)
}
let action: (String?) -> Void = { [weak self] emoticon in
@ -946,16 +961,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDeleg
strongSelf.entries = entries
strongSelf.themes = themes
if isDarkAppearance && selectedEmoticon == defaultEmoticon {
strongSelf.codeForegroundDimNode.alpha = 1.0
} else {
strongSelf.codeForegroundDimNode.alpha = isDarkAppearance ? 0.4 : 0.3
}
if strongSelf.codeForegroundContentNode == nil, let contentNode = strongSelf.wallpaperBackgroundNode.makeDimmedNode() {
contentNode.frame = CGRect(origin: CGPoint(x: -strongSelf.codeForegroundNode.frame.minX, y: -strongSelf.codeForegroundNode.frame.minY), size: strongSelf.wallpaperBackgroundNode.frame.size)
strongSelf.codeForegroundContentNode = contentNode
strongSelf.codeForegroundNode.insertSubnode(contentNode, at: 0)
}
strongSelf.contentNode.update(wallpaper: wallpaper, isDarkAppearance: isDarkAppearance, selectedEmoticon: selectedEmoticon)
if isFirstTime {
for theme in themes {
@ -1001,15 +1007,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDeleg
}
}
self.codeImageNode.setSignal(qrCode(string: "https://t.me/\(peer.addressName ?? "")", color: .black, backgroundColor: nil, icon: .cutout, ecl: "Q") |> beforeNext { [weak self] size, _ in
guard let strongSelf = self else {
return
}
strongSelf.qrCodeSize = size
if let (layout, navigationHeight) = strongSelf.containerLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate)
}
} |> map { $0.1 }, attemptSynchronously: true)
self.ready.set(self.contentNode.isReady)
}
private func enqueueTransition(_ transition: ThemeSettingsThemeItemNodeTransition) {
@ -1034,7 +1032,12 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDeleg
var scrollToItem: ListViewScrollToItem?
if !self.initialized {
scrollToItem = ListViewScrollToItem(index: 0, position: .bottom(-57.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Down)
if let index = transition.entries.firstIndex(where: { entry in
return entry.emoticon == self.initiallySelectedEmoticon
}) {
scrollToItem = ListViewScrollToItem(index: index, position: .bottom(-57.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Down)
self.initialized = true
}
self.initialized = true
}
@ -1080,23 +1083,18 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDeleg
override func didLoad() {
super.didLoad()
self.wrappingScrollNode.view.delegate = self
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.codeForegroundNode.view.mask = self.codeMaskNode.view
self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true
}
@objc func cancelButtonPressed() {
self.cancel?()
}
func dimTapped() {
self.cancelButtonPressed()
}
@objc func switchThemePressed() {
self.switchThemeButton.isUserInteractionEnabled = false
Queue.mainQueue().after(0.5) {
@ -1131,8 +1129,8 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDeleg
}
private func animateCrossfade(animateIcon: Bool) {
if let snapshotView = self.containerNode.view.snapshotView(afterScreenUpdates: false) {
self.wrappingScrollNode.view.insertSubview(snapshotView, aboveSubview: self.containerNode.view)
if let snapshotView = self.contentNode.containerNode.view.snapshotView(afterScreenUpdates: false) {
self.contentNode.view.insertSubview(snapshotView, aboveSubview: self.contentNode.containerNode.view)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreen.themeCrossfadeDuration, delay: ChatQrCodeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
@ -1180,15 +1178,12 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDeleg
private var animatedOut = false
func animateIn() {
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
let dimPosition = self.dimNode.layer.position
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
let targetBounds = self.bounds
self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset)
self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset)
transition.animateView({
self.bounds = targetBounds
self.dimNode.position = dimPosition
})
}
@ -1234,16 +1229,11 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDeleg
if backgroundFrame.minY < contentFrame.minY {
backgroundFrame.origin.y = contentFrame.minY
}
transition.updateFrame(node: self.wallpaperBackgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size))
self.wallpaperBackgroundNode.updateLayout(size: layout.size, transition: transition)
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
let titleSize = self.titleNode.measure(CGSize(width: width - 90.0, height: titleHeight))
let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 19.0 + UIScreenPixel), size: titleSize)
@ -1276,10 +1266,263 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDeleg
self.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0 + titleHeight + 6.0)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: contentSize.height, height: contentSize.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
let codeInset: CGFloat = 45.0
let codeBackgroundWidth = layout.size.width - codeInset * 2.0
self.contentNode.updateLayout(size: layout.size, topInset: 44.0, bottomInset: contentHeight, transition: transition)
transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: layout.size))
}
}
private class QrContentNode: ASDisplayNode {
private let context: AccountContext
private let peer: Peer
private let isStatic: Bool
fileprivate let containerNode: ASDisplayNode
fileprivate let wallpaperBackgroundNode: WallpaperBackgroundNode
private let codeBackgroundNode: ASDisplayNode
private let codeForegroundNode: ASDisplayNode
private var codeForegroundContentNode: ASDisplayNode?
private var codeForegroundDimNode: ASDisplayNode
private let codeMaskNode: ASDisplayNode
private let codeTextNode: ImmediateTextNode
private let codeImageNode: TransformImageNode
private let codeIconBackgroundNode: ASImageNode
private let codeStaticIconNode: ASImageNode?
private let codeAnimatedIconNode: AnimatedStickerNode?
private let avatarNode: ImageNode
private var qrCodeSize: Int?
private var currentParams: (TelegramWallpaper, Bool, String?)?
private var validLayout: (CGSize, CGFloat, CGFloat)?
private let _ready = Promise<Bool>()
var isReady: Signal<Bool, NoError> {
return self._ready.get()
}
init(context: AccountContext, peer: Peer, isStatic: Bool = false) {
self.context = context
self.peer = peer
self.isStatic = isStatic
self.containerNode = ASDisplayNode()
self.wallpaperBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: false, useExperimentalImplementation: context.sharedContext.immediateExperimentalUISettings.experimentalBackground)
self.codeBackgroundNode = ASDisplayNode()
self.codeBackgroundNode.backgroundColor = .white
self.codeBackgroundNode.cornerRadius = 42.0
if #available(iOS 13.0, *) {
self.codeBackgroundNode.layer.cornerCurve = .continuous
}
self.codeForegroundNode = ASDisplayNode()
self.codeForegroundNode.backgroundColor = .black
self.codeForegroundDimNode = ASDisplayNode()
self.codeForegroundDimNode.alpha = 0.3
self.codeForegroundDimNode.backgroundColor = .black
self.codeMaskNode = ASDisplayNode()
self.codeImageNode = TransformImageNode()
self.codeIconBackgroundNode = ASImageNode()
if isStatic {
let codeStaticIconNode = ASImageNode()
codeStaticIconNode.displaysAsynchronously = false
codeStaticIconNode.contentMode = .scaleToFill
codeStaticIconNode.image = UIImage(bundleImageName: "Share/QrPlaneIcon")
self.codeStaticIconNode = codeStaticIconNode
self.codeAnimatedIconNode = nil
} else {
let codeAnimatedIconNode = AnimatedStickerNode()
codeAnimatedIconNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "PlaneLogoPlain"), width: 120, height: 120, mode: .direct(cachePathPrefix: nil))
codeAnimatedIconNode.visibility = true
self.codeAnimatedIconNode = codeAnimatedIconNode
self.codeStaticIconNode = nil
}
self.codeTextNode = ImmediateTextNode()
self.codeTextNode.displaysAsynchronously = false
self.codeTextNode.attributedText = NSAttributedString(string: "@\(peer.addressName ?? "")".uppercased(), font: Font.with(size: 23.0, design: .round, weight: .bold, traits: []), textColor: .black)
self.codeTextNode.truncationMode = .byCharWrapping
self.codeTextNode.maximumNumberOfLines = 2
self.codeTextNode.textAlignment = .center
if isStatic {
self.codeTextNode.setNeedsDisplayAtScale(3.0)
}
self.avatarNode = ImageNode()
self.avatarNode.displaysAsynchronously = false
self.avatarNode.setSignal(peerAvatarCompleteImage(account: context.account, peer: EnginePeer(peer), size: CGSize(width: 180.0, height: 180.0), font: avatarPlaceholderFont(size: 78.0), fullSize: true))
super.init()
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.wallpaperBackgroundNode)
self.containerNode.addSubnode(self.codeBackgroundNode)
self.containerNode.addSubnode(self.codeForegroundNode)
self.codeForegroundNode.addSubnode(self.codeForegroundDimNode)
self.codeMaskNode.addSubnode(self.codeImageNode)
self.codeMaskNode.addSubnode(self.codeIconBackgroundNode)
self.codeMaskNode.addSubnode(self.codeTextNode)
self.containerNode.addSubnode(self.avatarNode)
if let codeStaticIconNode = self.codeStaticIconNode {
self.containerNode.addSubnode(codeStaticIconNode)
} else if let codeAnimatedIconNode = self.codeAnimatedIconNode {
self.addSubnode(codeAnimatedIconNode)
}
let codeReadyPromise = ValuePromise<Bool>()
self.codeImageNode.setSignal(qrCode(string: "https://t.me/\(peer.addressName ?? "")", 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)
}
codeReadyPromise.set(true)
} |> map { $0.1 }, attemptSynchronously: true)
self._ready.set(combineLatest(codeReadyPromise.get(), self.wallpaperBackgroundNode.isReady)
|> map { codeReady, wallpaperReady in
return codeReady && wallpaperReady
})
}
override func didLoad() {
super.didLoad()
self.codeForegroundNode.view.mask = self.codeMaskNode.view
}
func generateImage(completion: @escaping (UIImage?) -> Void) {
guard let (wallpaper, isDarkAppearance, selectedEmoticon) = self.currentParams else {
return
}
let size = CGSize(width: 390.0, height: 844.0)
let scale: CGFloat = 3.0
let copyNode = QrContentNode(context: self.context, peer: self.peer, isStatic: true)
func prepare(view: UIView, scale: CGFloat) {
view.contentScaleFactor = scale
for subview in view.subviews {
prepare(view: subview, scale: scale)
}
}
prepare(view: copyNode.view, scale: scale)
copyNode.updateLayout(size: size, topInset: 0.0, bottomInset: 0.0, transition: .immediate)
copyNode.update(wallpaper: wallpaper, isDarkAppearance: isDarkAppearance, selectedEmoticon: selectedEmoticon)
copyNode.frame = CGRect(x: -1000, y: -1000, width: size.width, height: size.height)
self.addSubnode(copyNode)
let _ = (copyNode.isReady
|> take(1)
|> deliverOnMainQueue).start(next: { [weak copyNode] _ in
Queue.mainQueue().after(0.1) {
if #available(iOS 10.0, *) {
let format = UIGraphicsImageRendererFormat()
format.scale = scale
let renderer = UIGraphicsImageRenderer(size: size, format: format)
let image = renderer.image { rendererContext in
copyNode?.containerNode.layer.render(in: rendererContext.cgContext)
}
completion(image)
} else {
UIGraphicsBeginImageContextWithOptions(size, true, scale)
copyNode?.containerNode.view.drawHierarchy(in: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
completion(image)
}
copyNode?.removeFromSupernode()
}
})
}
func update(wallpaper: TelegramWallpaper, isDarkAppearance: Bool, selectedEmoticon: String?) {
self.currentParams = (wallpaper, isDarkAppearance, selectedEmoticon)
self.wallpaperBackgroundNode.update(wallpaper: wallpaper)
if isDarkAppearance && selectedEmoticon == defaultEmoticon {
self.codeForegroundDimNode.alpha = 1.0
} else {
self.codeForegroundDimNode.alpha = isDarkAppearance ? 0.4 : 0.3
}
if self.codeForegroundContentNode == nil, let contentNode = self.wallpaperBackgroundNode.makeDimmedNode() {
contentNode.frame = CGRect(origin: CGPoint(x: -self.codeForegroundNode.frame.minX, y: -self.codeForegroundNode.frame.minY), size: self.wallpaperBackgroundNode.frame.size)
self.codeForegroundContentNode = contentNode
self.codeForegroundNode.insertSubnode(contentNode, at: 0)
}
if isDarkAppearance && selectedEmoticon == defaultEmoticon, let codeForegroundContentNode = self.codeForegroundContentNode {
codeForegroundContentNode.removeFromSupernode()
self.codeForegroundContentNode = nil
}
}
func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, topInset, bottomInset)
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size))
transition.updateFrame(node: self.wallpaperBackgroundNode, frame: CGRect(origin: CGPoint(), size: size))
self.wallpaperBackgroundNode.updateLayout(size: size, transition: transition)
let textLength = self.codeTextNode.attributedText?.string.count ?? 0
var topInset = topInset
let avatarSize: CGSize
let codeInset: CGFloat
let imageSide: CGFloat
let fontSize: CGFloat
if size.width > 320.0 {
avatarSize = CGSize(width: 100.0, height: 100.0)
codeInset = 45.0
imageSide = 220.0
if size.width > 375.0 {
if textLength > 12 {
fontSize = 22.0
} else {
fontSize = 24.0
}
} else {
if textLength > 12 {
fontSize = 21.0
} else {
fontSize = 23.0
}
}
} else {
avatarSize = CGSize(width: 70.0, height: 70.0)
codeInset = 55.0
imageSide = 160.0
topInset = floor(topInset * 0.6)
if textLength > 12 {
fontSize = 18.0
} else {
fontSize = 20.0
}
}
self.codeTextNode.attributedText = NSAttributedString(string: self.codeTextNode.attributedText?.string ?? "", font: Font.with(size: fontSize, design: .round, weight: .bold, traits: []), textColor: .black)
let codeBackgroundWidth = size.width - codeInset * 2.0
let codeBackgroundHeight = floor(codeBackgroundWidth * 1.1)
let codeBackgroundFrame = CGRect(x: codeInset, y: floor((layout.size.height - contentHeight - codeBackgroundHeight) / 2.0) + 44.0, width: codeBackgroundWidth, height: codeBackgroundHeight)
let codeBackgroundFrame = CGRect(x: codeInset, y: topInset + floor((size.height - bottomInset - codeBackgroundHeight) / 2.0), width: codeBackgroundWidth, height: codeBackgroundHeight)
transition.updateFrame(node: self.codeBackgroundNode, frame: codeBackgroundFrame)
transition.updateFrame(node: self.codeForegroundNode, frame: codeBackgroundFrame)
transition.updateFrame(node: self.codeMaskNode, frame: CGRect(origin: CGPoint(), size: codeBackgroundFrame.size))
@ -1290,33 +1533,38 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDeleg
}
let makeImageLayout = self.codeImageNode.asyncLayout()
let imageSide: CGFloat = 220.0
let imageSize = CGSize(width: imageSide, height: imageSide)
let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil))
let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil, scale: self.isStatic ? 3.0 : nil ))
let _ = imageApply()
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 codeTextSize = self.codeTextNode.updateLayout(codeBackgroundFrame.size)
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) - 7.0), size: codeTextSize))
let codeTextSize = self.codeTextNode.updateLayout(CGSize(width: codeBackgroundFrame.width - floor(imageFrame.minX * 1.5), 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 avatarSize = CGSize(width: 100.0, height: 100.0)
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - avatarSize.width) / 2.0), y: codeBackgroundFrame.minY - 70.0), size: avatarSize))
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 {
let (_, cutoutFrame, _) = qrCodeCutout(size: qrCodeSize, dimensions: imageSize, scale: nil)
self.codeIconNode.updateLayout(size: cutoutFrame.size)
let imageCenter = imageFrame.center.offsetBy(dx: codeBackgroundFrame.minX, dy: codeBackgroundFrame.minY)
if let codeStaticIconNode = self.codeStaticIconNode {
transition.updateBounds(node: codeStaticIconNode, bounds: CGRect(origin: CGPoint(), size: cutoutFrame.size))
transition.updatePosition(node: codeStaticIconNode, position: imageCenter.offsetBy(dx: 0.0, dy: -1.0))
} else if let codeAnimatedIconNode = self.codeAnimatedIconNode {
codeAnimatedIconNode.updateLayout(size: cutoutFrame.size)
transition.updateBounds(node: codeAnimatedIconNode, bounds: CGRect(origin: CGPoint(), size: cutoutFrame.size))
transition.updatePosition(node: codeAnimatedIconNode, position: imageCenter.offsetBy(dx: 0.0, dy: -1.0))
}
let backgroundSize = CGSize(width: floorToScreenPixels(cutoutFrame.width - 8.0), height: floorToScreenPixels(cutoutFrame.height - 8.0))
transition.updateFrame(node: self.codeIconBackgroundNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels(imageFrame.center.x - backgroundSize.width / 2.0), y: floorToScreenPixels(imageFrame.center.y - backgroundSize.height / 2.0)), size: backgroundSize))
if self.codeIconBackgroundNode.image == nil {
self.codeIconBackgroundNode.image = generateFilledCircleImage(diameter: backgroundSize.width, color: .black)
}
let imageCenter = imageFrame.center.offsetBy(dx: codeBackgroundFrame.minX, dy: codeBackgroundFrame.minY)
transition.updateBounds(node: self.codeIconNode, bounds: CGRect(origin: CGPoint(), size: cutoutFrame.size))
transition.updatePosition(node: self.codeIconNode, position: imageCenter.offsetBy(dx: 0.0, dy: -1.0))
}
}
}

View File

@ -185,15 +185,7 @@ private func matchingEmojiEntry(_ emoji: String) -> (UInt8, UInt8, UInt8)? {
func messageIsElligibleForLargeEmoji(_ message: Message) -> Bool {
if !message.text.isEmpty && message.text.containsOnlyEmoji && message.text.emojis.count < 4 {
var messageEntities: [MessageTextEntity]?
for attribute in message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
messageEntities = attribute.entities
break
}
}
if !(messageEntities?.isEmpty ?? true) {
if !(message.textEntitiesAttribute?.entities.isEmpty ?? true) {
return false
}

View File

@ -309,20 +309,6 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
}
}
let labelSize = self.labelNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
let textLayout = self.textNode.updateLayoutInfo(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
let textSize = textLayout.size
if case .multiLine = item.textBehavior, textLayout.truncated, !self.isExpanded {
self.expandBackgroundNode.isHidden = false
self.expandNode.isHidden = false
self.expandButonNode.isHidden = false
} else {
self.expandBackgroundNode.isHidden = true
self.expandNode.isHidden = true
self.expandButonNode.isHidden = true
}
if let icon = item.icon {
let iconImage: UIImage?
switch icon {
@ -337,6 +323,21 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
self.iconButtonNode.isHidden = true
}
let additionalSideInset: CGFloat = !self.iconNode.isHidden ? 32.0 : 0.0
let labelSize = self.labelNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
let textLayout = self.textNode.updateLayoutInfo(CGSize(width: width - sideInset * 2.0 - additionalSideInset, height: .greatestFiniteMagnitude))
let textSize = textLayout.size
if case .multiLine = item.textBehavior, textLayout.truncated, !self.isExpanded {
self.expandBackgroundNode.isHidden = false
self.expandNode.isHidden = false
self.expandButonNode.isHidden = false
} else {
self.expandBackgroundNode.isHidden = true
self.expandNode.isHidden = true
self.expandButonNode.isHidden = true
}
let labelFrame = CGRect(origin: CGPoint(x: sideInset, y: 11.0), size: labelSize)
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: labelFrame.maxY + 3.0), size: textSize)

View File

@ -192,7 +192,7 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode {
}, removePeer: { _ in
}, contextAction: item.contextAction, hasTopStripe: false, hasTopGroupInset: false, noInsets: true, displayDecorations: false)
}, contextAction: item.contextAction, hasTopStripe: false, hasTopGroupInset: false, noInsets: true, noCorners: true, displayDecorations: false)
let params = ListViewItemLayoutParams(width: width, leftInset: safeInsets.left, rightInset: safeInsets.right, availableHeight: 1000.0)

View File

@ -42,7 +42,7 @@ private struct GroupsInCommonListEntry: Comparable, Identifiable {
}, removePeer: { _ in
}, contextAction: { node, gesture in
openPeerContextAction(peer, node, gesture)
}, hasTopStripe: false, noInsets: true)
}, hasTopStripe: false, noInsets: true, noCorners: true)
}
}

View File

@ -3245,6 +3245,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
self?.paneContainerNode.currentPane?.node.ensureMessageIsVisible(id: messageId)
}))
}
private func openResolved(_ result: ResolvedUrl) {
guard let navigationController = self.controller?.navigationController as? NavigationController else {
return
@ -5479,35 +5480,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
guard let data = self.data, let peer = data.peer, let controller = self.controller else {
return
}
let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false)
|> map { animatedEmoji -> [String: [StickerPackItem]] in
var animatedEmojiStickers: [String: [StickerPackItem]] = [:]
switch animatedEmoji {
case let .result(_, items, _):
for item in items {
if let emoji = item.getStringRepresentationsOfIndexKeys().first {
animatedEmojiStickers[emoji.basicEmoji.0] = [item]
let strippedEmoji = emoji.basicEmoji.0.strippedEmoji
if animatedEmojiStickers[strippedEmoji] == nil {
animatedEmojiStickers[strippedEmoji] = [item]
}
}
}
default:
break
}
return animatedEmojiStickers
}
let _ = (animatedEmojiStickers
|> deliverOnMainQueue).start(next: { [weak self, weak controller] animatedEmojiStickers in
if let strongSelf = self, let controller = controller {
controller.present(ChatQrCodeScreen(context: strongSelf.context, animatedEmojiStickers: animatedEmojiStickers, peer: peer), in: .window(.root))
}
})
// controller.present(QrCodeScreen(context: self.context, updatedPresentationData: controller.updatedPresentationData, subject: .peer(peer: EnginePeer(peer))), in: .window(.root))
controller.present(ChatQrCodeScreen(context: self.context, peer: peer), in: .window(.root))
}
fileprivate func openSettings(section: PeerInfoSettingsSection) {

View File

@ -1414,6 +1414,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: context.engine.privacy.webSessions(), websitesOnly: false)
}
public func makeChatQrCodeScreen(context: AccountContext, peer: Peer) -> ViewController {
return ChatQrCodeScreen(context: context, peer: peer)
}
public func makePrivacyAndSecurityController(context: AccountContext) -> ViewController {
return SettingsUI.makePrivacyAndSecurityController(context: context)
}

View File

@ -40,10 +40,11 @@ public func canTranslateText(context: AccountContext, text: String, showTranslat
let text = String(text.prefix(64))
languageRecognizer.processString(text)
let hypotheses = languageRecognizer.languageHypotheses(withMaximum: 2)
let hypotheses = languageRecognizer.languageHypotheses(withMaximum: 3)
languageRecognizer.reset()
if let language = hypotheses.first(where: { supportedTranslationLanguages.contains($0.key.rawValue) }) {
let filteredLanguages = hypotheses.filter { supportedTranslationLanguages.contains($0.key.rawValue) }.sorted(by: { $0.value > $1.value })
if let language = filteredLanguages.first(where: { supportedTranslationLanguages.contains($0.key.rawValue) }) {
return !dontTranslateLanguages.contains(language.key.rawValue)
} else {
return false

View File

@ -1740,18 +1740,18 @@ private class WallpaperNewYearNode: ASDisplayNode {
let cell1 = CAEmitterCell()
cell1.contents = UIImage(bundleImageName: "Components/Snowflake")?.cgImage
cell1.name = "snow"
cell1.birthRate = 352.0
cell1.birthRate = 252.0
cell1.lifetime = 20.0
cell1.velocity = 39.0
cell1.velocityRange = -15.0
cell1.xAcceleration = 5.0
cell1.yAcceleration = 25.0
cell1.velocity = 19.0
cell1.velocityRange = -5.0
cell1.xAcceleration = 2.5
cell1.yAcceleration = 10.0
cell1.emissionRange = .pi
cell1.spin = -28.6 * (.pi / 180.0)
cell1.spinRange = 57.2 * (.pi / 180.0)
cell1.scale = 0.04
cell1.scaleRange = 0.15
cell1.color = UIColor.white.withAlphaComponent(0.88).cgColor
cell1.color = UIColor.white.withAlphaComponent(0.58).cgColor
// cell1.alphaRange = -0.2
particlesLayer.emitterCells = [cell1]