diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 08524f9e6c..f6c11d3ca3 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 7065a4f7e4..f014e50870 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -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 { get } var immediateHasOngoingCall: Bool { get } diff --git a/submodules/AuthTransferUI/BUILD b/submodules/AuthTransferUI/BUILD index 59ad1f59bd..805946bb57 100644 --- a/submodules/AuthTransferUI/BUILD +++ b/submodules/AuthTransferUI/BUILD @@ -30,6 +30,7 @@ swift_library( "//submodules/UndoUI:UndoUI", "//submodules/TextFormat:TextFormat", "//submodules/Markdown:Markdown", + "//submodules/QrCodeUI:QrCodeUI", ], visibility = [ "//visibility:public", diff --git a/submodules/AuthTransferUI/Sources/AuthTransferConfirmationScreen.swift b/submodules/AuthTransferUI/Sources/AuthTransferConfirmationScreen.swift index ccb27eaeeb..75660fb5a4 100644 --- a/submodules/AuthTransferUI/Sources/AuthTransferConfirmationScreen.swift +++ b/submodules/AuthTransferUI/Sources/AuthTransferConfirmationScreen.swift @@ -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) }) }) diff --git a/submodules/ContactListUI/BUILD b/submodules/ContactListUI/BUILD index 733cb8688e..7d77adb63e 100644 --- a/submodules/ContactListUI/BUILD +++ b/submodules/ContactListUI/BUILD @@ -35,6 +35,7 @@ swift_library( "//submodules/StickerResources:StickerResources", "//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", + "//submodules/QrCodeUI:QrCodeUI", ], visibility = [ "//visibility:public", diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index f1b92630d8..58793e4d82 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -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() } diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index e0fa6d6573..0c68933b2d 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -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) } diff --git a/submodules/DeviceAccess/Sources/DeviceAccess.swift b/submodules/DeviceAccess/Sources/DeviceAccess.swift index c3aabcf48b..6f5642e2da 100644 --- a/submodules/DeviceAccess/Sources/DeviceAccess.swift +++ b/submodules/DeviceAccess/Sources/DeviceAccess.swift @@ -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() diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 5cff2c7bbd..72ec3d63b0 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -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)) diff --git a/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift b/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift index 8f5713c25c..6aac0ba01f 100644 --- a/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift +++ b/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift @@ -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 } diff --git a/submodules/ImageContentAnalysis/Sources/ImageContentAnalysis.swift b/submodules/ImageContentAnalysis/Sources/ImageContentAnalysis.swift index 8ebeb58f25..85654b1bcf 100644 --- a/submodules/ImageContentAnalysis/Sources/ImageContentAnalysis.swift +++ b/submodules/ImageContentAnalysis/Sources/ImageContentAnalysis.swift @@ -351,3 +351,33 @@ public func recognizedContent(postbox: Postbox, image: @escaping () -> UIImage?, } } } + +public func recognizeQRCode(in image: UIImage?) -> Signal { + 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) + } +} diff --git a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift index 655e9651b1..f237be32fd 100644 --- a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift @@ -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 diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index 12d2e57962..d5b76e5ce9 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -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 { diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyWallpaperPicker.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyWallpaperPicker.swift index 05968635c7..a556175eea 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyWallpaperPicker.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyWallpaperPicker.swift @@ -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 diff --git a/submodules/QrCodeUI/BUILD b/submodules/QrCodeUI/BUILD index 159c83a8c5..0308b4224b 100644 --- a/submodules/QrCodeUI/BUILD +++ b/submodules/QrCodeUI/BUILD @@ -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", diff --git a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift new file mode 100644 index 0000000000..c96a5a07d0 --- /dev/null +++ b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift @@ -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 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(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 + } +} + diff --git a/submodules/QrCodeUI/Sources/QrCodeScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScreen.swift index b6aadc72ae..707dc8d2f2 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScreen.swift @@ -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() diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index aa68c921f7..7c0f25465a 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -93,6 +93,7 @@ swift_library( "//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode", "//submodules/WebPBinding:WebPBinding", "//submodules/Translate:Translate", + "//submodules/QrCodeUI:QrCodeUI", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift index 340cfaed54..db65fcdc95 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift @@ -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: {}) diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index ac4db6b397..ec6a220a46 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -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) } } diff --git a/submodules/TelegramPermissionsUI/Sources/PermissionController.swift b/submodules/TelegramPermissionsUI/Sources/PermissionController.swift index dc2040b2a5..9589dcc71d 100644 --- a/submodules/TelegramPermissionsUI/Sources/PermissionController.swift +++ b/submodules/TelegramPermissionsUI/Sources/PermissionController.swift @@ -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?() } diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift index 781c2a4f4d..50213ed33a 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift @@ -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) diff --git a/submodules/TelegramPresentationData/Sources/PresentationData.swift b/submodules/TelegramPresentationData/Sources/PresentationData.swift index 27550e256a..12f73d381a 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationData.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationData.swift @@ -309,7 +309,6 @@ public func currentPresentationDataAndSettings(accountManager: AccountManager> +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 \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Share/QrPlaneIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Share/QrPlaneIcon.imageset/Contents.json new file mode 100644 index 0000000000..de3bb69a15 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Share/QrPlaneIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_qrlogo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Share/QrPlaneIcon.imageset/ic_qrlogo.pdf b/submodules/TelegramUI/Images.xcassets/Share/QrPlaneIcon.imageset/ic_qrlogo.pdf new file mode 100644 index 0000000000..8d4ada089c Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Share/QrPlaneIcon.imageset/ic_qrlogo.pdf differ diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 87f7002b50..a60ba03cee 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift b/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift index 2f142ff0f0..ba593996a9 100644 --- a/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift +++ b/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift @@ -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() + 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 + private var selectedEmoticonPromise = ValuePromise(nil) private var isDarkAppearancePromise: ValuePromise 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 + 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() + var isReady: Signal { + 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() + 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)) } } } diff --git a/submodules/TelegramUI/Sources/EmojiResources.swift b/submodules/TelegramUI/Sources/EmojiResources.swift index 9c104fff3d..ac58b2ae1e 100644 --- a/submodules/TelegramUI/Sources/EmojiResources.swift +++ b/submodules/TelegramUI/Sources/EmojiResources.swift @@ -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 } diff --git a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift index 67b0302480..8c17da1cdb 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift @@ -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) diff --git a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift index b1e3bc666b..457e781c73 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift @@ -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) diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift index 786fd31eda..b8cf88124d 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift @@ -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) } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index fc84b8c5f4..bcb80f9050 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -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) { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 27ee669a26..036bd5ca2a 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -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) } diff --git a/submodules/Translate/Sources/Translate.swift b/submodules/Translate/Sources/Translate.swift index 984f805382..836f825fd5 100644 --- a/submodules/Translate/Sources/Translate.swift +++ b/submodules/Translate/Sources/Translate.swift @@ -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 diff --git a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift index 8105368973..d133ee81ac 100644 --- a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift +++ b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift @@ -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]