diff --git a/Telegram/Telegram-iOS/Resources/PlaneLogoPlain.tgs b/Telegram/Telegram-iOS/Resources/PlaneLogoPlain.tgs new file mode 100644 index 0000000000..1bfbf196d7 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/PlaneLogoPlain.tgs differ diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 21c11250d4..5074a8ff8e 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7145,3 +7145,7 @@ Sorry for the inconvenience."; "ChatList.Archive" = "Archive"; "TextFormat.Spoiler" = "Spoiler"; + +"Conversation.ContextMenuTranslate" = "Translate"; + +"ClearCache.ClearDescription" = "All media will stay in the Telegram cloud and can be re-downloaded if you need it again."; diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 5123ae2fd3..652f74211d 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -61,6 +61,7 @@ swift_library( "//submodules/TelegramStringFormatting:TelegramStringFormatting", "//submodules/TelegramCallsUI:TelegramCallsUI", "//submodules/StickerResources:StickerResources", + "//submodules/TextFormat:TextFormat", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 17b5c9cf6e..6db0fac958 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -17,6 +17,8 @@ import PhotoResources import ChatListSearchItemNode import ContextUI import ChatInterfaceState +import TextFormat +import InvisibleInkDustNode public enum ChatListItemContent { public final class DraftState: Equatable { @@ -427,6 +429,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let measureNode: TextNode private var currentItemHeight: CGFloat? let textNode: TextNode + var dustNode: InvisibleInkDustNode? let inputActivitiesNode: ChatListInputActivitiesNode let dateNode: TextNode let separatorNode: ASDisplayNode @@ -1049,12 +1052,26 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { attributedText = NSAttributedString(string: foldLineBreaks(draftText.replacingOccurrences(of: "\n\n", with: " ")), font: textFont, textColor: theme.messageTextColor) } else if let message = messages.last { var composedString: NSMutableAttributedString + + let entities = (message._asMessage().textEntitiesAttribute?.entities ?? []).filter { entity in + if case .Spoiler = entity.type { + return true + } else { + return false + } + } + let messageString: NSAttributedString + if !message.text.isEmpty { + messageString = stringWithAppliedEntities(message.text, entities: entities, baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false) + } else { + messageString = NSAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor) + } if let inlineAuthorPrefix = inlineAuthorPrefix { composedString = NSMutableAttributedString() composedString.append(NSAttributedString(string: "\(inlineAuthorPrefix): ", font: textFont, textColor: theme.titleColor)) - composedString.append(NSAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor)) + composedString.append(messageString) } else { - composedString = NSMutableAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor) + composedString = NSMutableAttributedString(attributedString: messageString) } if let searchQuery = item.interaction.searchTextHighightState { @@ -1395,7 +1412,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { textCutout = TextNodeCutout(topLeft: CGSize(width: textLeftCutout, height: 10.0), topRight: nil, bottomRight: nil) } let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: textAttributedString, backgroundColor: nil, maximumNumberOfLines: authorAttributedString == nil ? 2 : 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth - badgeSize, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: textCutout, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) - + let titleRectWidth = rawContentWidth - dateLayout.size.width - 10.0 - statusWidth - titleIconsWidth let (titleLayout, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleRectWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -1730,6 +1747,24 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let textNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.minY + titleLayout.size.height - 1.0 + UIScreenPixel + (authorLayout.size.height.isZero ? 0.0 : (authorLayout.size.height - 3.0))), size: textLayout.size) strongSelf.textNode.frame = textNodeFrame + if !textLayout.spoilers.isEmpty { + let dustNode: InvisibleInkDustNode + if let current = strongSelf.dustNode { + dustNode = current + } else { + dustNode = InvisibleInkDustNode(textNode: nil) + dustNode.isUserInteractionEnabled = false + strongSelf.dustNode = dustNode + strongSelf.contextContainer.insertSubnode(dustNode, aboveSubnode: strongSelf.textNode) + } + dustNode.update(size: textNodeFrame.size, color: theme.messageTextColor, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) + dustNode.frame = textNodeFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) + + } else if let dustNode = strongSelf.dustNode { + strongSelf.dustNode = nil + dustNode.removeFromSupernode() + } + var animateInputActivitiesFrame = false let inputActivities = inputActivities?.filter({ switch $0.1 { diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 3ada505314..cd01fdb089 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -81,6 +81,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case enableDebugDataDisplay(Bool) case acceleratedStickers(Bool) case experimentalBackground(Bool) + case snow(Bool) case playerEmbedding(Bool) case playlistPlayback(Bool) case voiceConference @@ -102,7 +103,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logging.rawValue case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries: return DebugControllerSection.experiments.rawValue - case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .experimentalBackground: + case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .experimentalBackground, .snow: return DebugControllerSection.experiments.rawValue case .preferredVideoCodec: return DebugControllerSection.videoExperiments.rawValue @@ -173,14 +174,16 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 29 case .experimentalBackground: return 30 - case .playerEmbedding: + case .snow: return 31 - case .playlistPlayback: + case .playerEmbedding: return 32 - case .voiceConference: + case .playlistPlayback: return 33 + case .voiceConference: + return 34 case let .preferredVideoCodec(index, _, _, _): - return 34 + index + return 35 + index case .disableVideoAspectScaling: return 100 case .enableVoipTcp: @@ -814,6 +817,16 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) + case let .snow(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Snow", value: value, sectionId: self.section, style: .blocks, updated: { value in + let _ = arguments.sharedContext.accountManager.transaction ({ transaction in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in + var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings + settings.snow = value + return PreferencesEntry(settings) + }) + }).start() + }) case let .playerEmbedding(value): return ItemListSwitchItem(presentationData: presentationData, title: "Player Embedding", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in @@ -927,6 +940,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.enableDebugDataDisplay(experimentalSettings.enableDebugDataDisplay)) entries.append(.acceleratedStickers(experimentalSettings.acceleratedStickers)) entries.append(.experimentalBackground(experimentalSettings.experimentalBackground)) + entries.append(.snow(experimentalSettings.snow)) entries.append(.playerEmbedding(experimentalSettings.playerEmbedding)) entries.append(.playlistPlayback(experimentalSettings.playlistPlayback)) } diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 33ca210f87..ee16bed593 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -1005,6 +1005,10 @@ public class TextNode: ASDisplayNode { } let lineRange = CFRange(location: lastLineCharacterIndex, length: stringLength - lastLineCharacterIndex) + var brokenLineRange = CFRange(location: lastLineCharacterIndex, length: lineCharacterCount) + if brokenLineRange.location + brokenLineRange.length > attributedString.length { + brokenLineRange.length = attributedString.length - brokenLineRange.location + } if lineRange.length == 0 { break } @@ -1033,7 +1037,7 @@ public class TextNode: ASDisplayNode { } var headIndent: CGFloat = 0.0 - attributedString.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in + attributedString.enumerateAttributes(in: NSMakeRange(brokenLineRange.location, brokenLineRange.length), options: []) { attributes, range, _ in if let _ = attributes[NSAttributedString.Key.init(rawValue: "TelegramSpoiler")] { var ascent: CGFloat = 0.0 var descent: CGFloat = 0.0 diff --git a/submodules/GalleryUI/BUILD b/submodules/GalleryUI/BUILD index 87f0008595..71887c2ba2 100644 --- a/submodules/GalleryUI/BUILD +++ b/submodules/GalleryUI/BUILD @@ -39,6 +39,7 @@ swift_library( "//submodules/Speak:Speak", "//submodules/UndoUI:UndoUI", "//submodules/InvisibleInkDustNode:InvisibleInkDustNode", + "//submodules/Translate:Translate", ], visibility = [ "//visibility:public", diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index 824f19c07b..1e32f64027 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -16,6 +16,7 @@ import PresentationDataUtils import ImageContentAnalysis import TextSelectionNode import Speak +import Translate import ShareController import UndoUI @@ -352,6 +353,8 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { } case .speak: speakText(string) + case .translate: + translateText(context: strongSelf.context, text: string) } }) recognizedContentNode.barcodeAction = { [weak self] payload, rect in diff --git a/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift b/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift index 64fcc28b6d..dc42d26acf 100644 --- a/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift +++ b/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift @@ -203,6 +203,7 @@ public enum RecognizedTextSelectionAction { case share case lookup case speak + case translate } public final class RecognizedTextSelectionNode: ASDisplayNode { @@ -509,6 +510,12 @@ public final class RecognizedTextSelectionNode: ASDisplayNode { self?.performAction(selectedText, .lookup) let _ = self?.dismissSelection() })) +// if #available(iOS 15.0, *) { +// actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuTranslate, accessibilityLabel: self.strings.Conversation_ContextMenuTranslate), action: { [weak self] in +// self?.performAction(selectedText, .translate) +// let _ = self?.dismissSelection() +// })) +// } if isSpeakSelectionEnabled() { actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuSpeak, accessibilityLabel: self.strings.Conversation_ContextMenuSpeak), action: { [weak self] in self?.performAction(selectedText, .speak) diff --git a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift index cdded32c83..6974a73a49 100644 --- a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift @@ -62,7 +62,7 @@ public class InvisibleInkDustNode: ASDisplayNode { public var isRevealed = false - public init(textNode: TextNode) { + public init(textNode: TextNode?) { self.textNode = textNode self.emitterNode = ASDisplayNode() @@ -144,7 +144,7 @@ public class InvisibleInkDustNode: ASDisplayNode { } @objc private func tap(_ gestureRecognizer: UITapGestureRecognizer) { - guard let (size, _, _) = self.currentParams, !self.isRevealed else { + guard let (size, _, _) = self.currentParams, let textNode = self.textNode, !self.isRevealed else { return } @@ -155,15 +155,15 @@ public class InvisibleInkDustNode: ASDisplayNode { self.emitterLayer?.setValue(position, forKeyPath: "emitterBehaviors.fingerAttractor.position") Queue.mainQueue().after(0.1 * UIView.animationDurationFactor()) { - self.textNode?.view.mask = self.textMaskNode.view - self.textNode?.alpha = 1.0 + textNode.view.mask = self.textMaskNode.view + textNode.alpha = 1.0 let radius = max(size.width, size.height) self.textSpotNode.frame = CGRect(x: position.x - radius / 2.0, y: position.y - radius / 2.0, width: radius, height: radius) self.textSpotNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - self.textSpotNode.layer.animateScale(from: 0.1, to: 3.5, duration: 0.71, removeOnCompletion: false, completion: { [weak self] _ in - self?.textNode?.view.mask = nil + self.textSpotNode.layer.animateScale(from: 0.1, to: 3.5, duration: 0.71, removeOnCompletion: false, completion: { _ in + textNode.view.mask = nil }) self.emitterNode.view.mask = self.emitterMaskNode.view @@ -187,7 +187,7 @@ public class InvisibleInkDustNode: ASDisplayNode { } - let textLength = CGFloat((self.textNode?.cachedLayout?.attributedString?.string ?? "").count) + let textLength = CGFloat((textNode.cachedLayout?.attributedString?.string ?? "").count) let timeToRead = min(45.0, ceil(max(4.0, textLength * 0.04))) Queue.mainQueue().after(timeToRead * UIView.animationDurationFactor()) { self.isRevealed = false @@ -195,9 +195,7 @@ public class InvisibleInkDustNode: ASDisplayNode { let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .linear) transition.updateAlpha(node: self, alpha: 1.0) - if let textNode = self.textNode { - transition.updateAlpha(node: textNode, alpha: 0.0) - } + transition.updateAlpha(node: textNode, alpha: 0.0) } } diff --git a/submodules/QrCodeUI/Sources/QrCodeScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScreen.swift index 366b0a84ff..b6aadc72ae 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScreen.swift @@ -267,8 +267,7 @@ public final class QrCodeScreen: ViewController { self.qrImageNode.clipsToBounds = true self.qrImageNode.cornerRadius = 16.0 - self.qrIconNode = AnimatedStickerNode() - + self.qrIconNode = AnimatedStickerNode() self.qrIconNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "PlaneLogo"), width: 240, height: 240, mode: .direct(cachePathPrefix: nil)) self.qrIconNode.visibility = true @@ -434,7 +433,6 @@ public final class QrCodeScreen: ViewController { let imageSide: CGFloat = 240.0 let imageSize = CGSize(width: imageSide, height: imageSide) let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil)) - let _ = imageApply() let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) diff --git a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift index b6bd4802fd..4939d18b57 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift @@ -521,6 +521,8 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P var totalSize: Int64 = 0 + items.append(ActionSheetTextItem(title: presentationData.strings.ClearCache_ClearDescription)) + for categoryId in validCategories { if let (_, size) = sizeIndex[categoryId] { let categorySize: Int64 = size diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift index bfc12f83f1..ef9d32fa65 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift @@ -150,7 +150,7 @@ class RecentSessionsHeaderItemNode: ListViewItemNode { strongSelf.buttonNode.title = item.context.sharedContext.currentPresentationData.with { $0 }.strings.AuthSessions_LinkDesktopDevice if let _ = updatedTheme { - strongSelf.buttonNode.icon = generateTintedImage(image: UIImage(bundleImageName: "Settings/QrButtonIcon"), color: .white) + strongSelf.buttonNode.icon = UIImage(bundleImageName: "Settings/QrButtonIcon") strongSelf.buttonNode.updateTheme(SolidRoundedButtonTheme(theme: item.theme)) } diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index 6f0b95f0f4..eaca96a86b 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -69,7 +69,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode { public var icon: UIImage? { didSet { - self.iconNode.image = generateTintedImage(image: self.iconNode.image, color: self.theme.foregroundColor) + self.iconNode.image = generateTintedImage(image: self.icon, color: self.theme.foregroundColor) } } diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index 5d804ee1a5..ff7393ebaf 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -322,6 +322,15 @@ public extension Message { } return nil } + + var textEntitiesAttribute: TextEntitiesMessageAttribute? { + for attribute in self.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + return attribute + } + } + return nil + } } public func _internal_parseMediaAttachment(data: Data) -> Media? { diff --git a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift index e51050345e..bbd4720999 100644 --- a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift +++ b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift @@ -229,12 +229,13 @@ public func stringForMediaKind(_ kind: MessageContentKind, strings: Presentation } } -public func descriptionStringForMessage(contentSettings: ContentSettings, message: EngineMessage, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: EnginePeer.Id) -> (String, Bool) { +public func descriptionStringForMessage(contentSettings: ContentSettings, message: EngineMessage, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: EnginePeer.Id) -> (String, Bool, Bool) { let contentKind = messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId) if !message.text.isEmpty && ![.expiredImage, .expiredVideo].contains(contentKind.key) { - return (foldLineBreaks(message.text), false) + return (foldLineBreaks(message.text), false, true) } - return stringForMediaKind(contentKind, strings: strings) + let result = stringForMediaKind(contentKind, strings: strings) + return (result.0, result.1, false) } public func foldLineBreaks(_ text: String) -> String { diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index d0f2a6d0de..3a3ff87bed 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -251,6 +251,7 @@ swift_library( "//submodules/InvisibleInkDustNode:InvisibleInkDustNode", "//submodules/QrCodeUI:QrCodeUI", "//submodules/Components/ReactionListContextMenuContent:ReactionListContextMenuContent", + "//submodules/Translate:Translate", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/Contents.json new file mode 100644 index 0000000000..1ce4a1d175 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "translate_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/translate_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/translate_24.pdf new file mode 100644 index 0000000000..7b61f5ea2e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/translate_24.pdf @@ -0,0 +1,127 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 8.335022 7.270081 cm +0.000000 0.000000 0.000000 scn +5.135226 14.200185 m +4.875527 14.459883 4.454473 14.459883 4.194774 14.200185 c +3.935075 13.940486 3.935075 13.519431 4.194774 13.259732 c +6.194774 11.259732 l +6.454473 11.000034 6.875527 11.000034 7.135226 11.259732 c +7.394925 11.519431 7.394925 11.940486 7.135226 12.200185 c +5.135226 14.200185 l +h +11.665000 9.064959 m +10.317697 9.064959 l +10.212673 6.276149 9.432839 4.057345 7.885226 2.509732 c +7.853345 2.477852 7.821179 2.446297 7.788730 2.415067 c +9.029876 1.748239 10.640882 1.394958 12.665000 1.394958 c +13.032269 1.394958 13.330000 1.097227 13.330000 0.729958 c +13.330000 0.362688 13.032269 0.064959 12.665000 0.064959 c +10.248617 0.064959 8.228645 0.535731 6.665002 1.532337 c +5.101360 0.535731 3.081383 0.064959 0.665000 0.064959 c +0.297731 0.064959 0.000000 0.362688 0.000000 0.729958 c +0.000000 1.097227 0.297731 1.394958 0.665000 1.394958 c +2.689117 1.394958 4.300128 1.748238 5.541274 2.415066 c +5.508824 2.446296 5.476655 2.477852 5.444774 2.509732 c +3.897161 4.057345 3.117327 6.276149 3.012303 9.064959 c +1.665000 9.064959 l +1.297731 9.064959 1.000000 9.362689 1.000000 9.729959 c +1.000000 10.097228 1.297731 10.394958 1.665000 10.394958 c +3.665000 10.394958 l +9.665000 10.394958 l +11.665000 10.394958 l +12.032269 10.394958 12.330000 10.097228 12.330000 9.729959 c +12.330000 9.362689 12.032269 9.064959 11.665000 9.064959 c +h +4.343254 9.064959 m +8.986746 9.064959 l +8.882931 6.515748 8.171854 4.677263 6.944774 3.450184 c +6.854816 3.360226 6.761571 3.273041 6.665002 3.188664 c +6.568432 3.273041 6.475184 3.360226 6.385226 3.450184 c +5.158146 4.677263 4.447069 6.515748 4.343254 9.064959 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 2.334839 3.245422 cm +0.000000 0.000000 0.000000 scn +5.780663 12.006347 m +5.678422 12.256270 5.435204 12.419556 5.165175 12.419556 c +4.895147 12.419556 4.651928 12.256270 4.549686 12.006347 c +0.049686 1.006347 l +-0.089374 0.666422 0.073459 0.278128 0.413384 0.139067 c +0.753309 0.000007 1.141604 0.162840 1.280664 0.502765 c +2.747988 4.089556 l +7.582363 4.089556 l +9.049686 0.502765 l +9.188747 0.162840 9.577042 0.000007 9.916966 0.139067 c +10.256891 0.278128 10.419724 0.666422 10.280664 1.006347 c +5.780663 12.006347 l +h +7.038272 5.419556 m +5.165175 9.998237 l +3.292078 5.419556 l +7.038272 5.419556 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 2388 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.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 +0000002478 00000 n +0000002501 00000 n +0000002674 00000 n +0000002748 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2807 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 0ce59576db..d6da13ca51 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -60,6 +60,7 @@ import InviteLinksUI import Markdown import TelegramPermissionsUI import Speak +import Translate import UniversalMediaPlayer import WallpaperBackgroundNode import ChatListUI @@ -2759,6 +2760,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } case .speak: speakText(text.string) + case .translate: + translateText(context: context, text: text.string) } }, displayImportedMessageTooltip: { [weak self] _ in guard let strongSelf = self else { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 3a494304d4..3da3ea5a96 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -760,6 +760,15 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState f(.default) }))) + if #available(iOS 15.0, *) { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuTranslate, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + controllerInteraction.performTextSelectionAction(0, NSAttributedString(string: message.text), .translate) + f(.default) + }))) + } + if isSpeakSelectionEnabled() && !message.text.isEmpty { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuSpeak, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Message"), color: theme.actionSheet.primaryTextColor) diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index c443c5136c..f497e2031c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -893,7 +893,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - if item.associatedData.isCopyProtectionEnabled { + if item.associatedData.isCopyProtectionEnabled || item.message.isCopyProtected() { needsShareButton = false } } diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 26bbb5a99a..6dcaa1dfe0 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -1161,7 +1161,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - if item.associatedData.isCopyProtectionEnabled { + if item.associatedData.isCopyProtectionEnabled || item.message.isCopyProtected() { needsShareButton = false } } diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index 4027a3fc32..9c9a19dd9c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -351,7 +351,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } } - if item.associatedData.isCopyProtectionEnabled { + if item.associatedData.isCopyProtectionEnabled || item.message.isCopyProtected() { needsShareButton = false } } diff --git a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift index 10c2a1ac0d..73eafa58ac 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift @@ -10,6 +10,8 @@ import AccountContext import LocalizedPeerData import PhotoResources import TelegramStringFormatting +import TextFormat +import InvisibleInkDustNode enum ChatMessageReplyInfoType { case bubble(incoming: Bool) @@ -21,6 +23,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { private let lineNode: ASImageNode private var titleNode: TextNode? private var textNode: TextNode? + private var dustNode: InvisibleInkDustNode? private var imageNode: TransformImageNode? private var previousMediaReference: AnyMediaReference? @@ -64,7 +67,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { } } - let (textString, isMedia) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: context.account.peerId) + let (textString, isMedia, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: context.account.peerId) let placeholderColor: UIColor = message.effectivelyIncoming(context.account.peerId) ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor let titleColor: UIColor @@ -89,6 +92,21 @@ class ChatMessageReplyInfoNode: ASDisplayNode { textColor = titleColor } + + let messageText: NSAttributedString + if isText { + let entities = (message.textEntitiesAttribute?.entities ?? []).filter { entity in + if case .Spoiler = entity.type { + return true + } else { + return false + } + } + messageText = stringWithAppliedEntities(message.text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false) + } else { + messageText = NSAttributedString(string: textString, font: textFont, textColor: textColor) + } + var leftInset: CGFloat = 11.0 let spacing: CGFloat = 2.0 @@ -131,7 +149,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { let textInsets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0) let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets)) - let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: textString, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets)) + let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets)) let imageSide = titleLayout.size.height + textLayout.size.height - 16.0 @@ -218,8 +236,27 @@ class ChatMessageReplyInfoNode: ASDisplayNode { node.imageNode?.captureProtected = message.isCopyProtected() titleNode.frame = CGRect(origin: CGPoint(x: leftInset - textInsets.left, y: spacing - textInsets.top), size: titleLayout.size) - textNode.frame = CGRect(origin: CGPoint(x: leftInset - textInsets.left, y: titleNode.frame.maxY - textInsets.bottom + spacing - textInsets.top), size: textLayout.size) + let textFrame = CGRect(origin: CGPoint(x: leftInset - textInsets.left, y: titleNode.frame.maxY - textInsets.bottom + spacing - textInsets.top), size: textLayout.size) + textNode.frame = textFrame + + if !textLayout.spoilers.isEmpty { + let dustNode: InvisibleInkDustNode + if let current = node.dustNode { + dustNode = current + } else { + dustNode = InvisibleInkDustNode(textNode: nil) + dustNode.isUserInteractionEnabled = false + node.dustNode = dustNode + node.contentNode.insertSubnode(dustNode, aboveSubnode: textNode) + } + dustNode.update(size: textFrame.size, color: titleColor, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) + dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) + } else if let dustNode = node.dustNode { + dustNode.removeFromSupernode() + node.dustNode = nil + } + node.lineNode.image = lineImage node.lineNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 3.0), size: CGSize(width: 2.0, height: max(0.0, size.height - 5.0))) diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index 856bf74daa..a35f84bc62 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -436,7 +436,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - if item.associatedData.isCopyProtectionEnabled { + if item.associatedData.isCopyProtectionEnabled || item.message.isCopyProtected() { needsShareButton = false } } diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index 167cdfa360..2dd980e9bd 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -374,6 +374,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { if let (_, spoilerTextApply) = spoilerTextLayoutAndApply { let spoilerTextNode = spoilerTextApply() if strongSelf.spoilerTextNode == nil { + spoilerTextNode.alpha = 0.0 spoilerTextNode.isUserInteractionEnabled = false spoilerTextNode.contentMode = .topLeft spoilerTextNode.contentsScale = UIScreenScale @@ -384,8 +385,6 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } strongSelf.spoilerTextNode?.frame = textFrame - strongSelf.spoilerTextNode?.isHidden = false - strongSelf.spoilerTextNode?.alpha = 0.0 let dustNode: InvisibleInkDustNode if let current = strongSelf.dustNode { @@ -395,11 +394,16 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.dustNode = dustNode strongSelf.insertSubnode(dustNode, aboveSubnode: spoilerTextNode) } - dustNode.update(size: textFrame.size, color: messageTheme.primaryTextColor, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) + dustNode.update(size: textFrame.size, color: messageTheme.secondaryTextColor, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) } else if let spoilerTextNode = strongSelf.spoilerTextNode { strongSelf.spoilerTextNode = nil spoilerTextNode.removeFromSupernode() + + if let dustNode = strongSelf.dustNode { + strongSelf.dustNode = nil + dustNode.removeFromSupernode() + } } if let textSelectionNode = strongSelf.textSelectionNode { @@ -632,7 +636,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { override func updateIsExtractedToContextPreview(_ value: Bool) { if value { - if self.textSelectionNode == nil, let item = self.item, !item.associatedData.isCopyProtectionEnabled, let rootNode = item.controllerInteraction.chatControllerNode() { + if self.textSelectionNode == nil, let item = self.item, !item.associatedData.isCopyProtectionEnabled && !item.message.isCopyProtected(), let rootNode = item.controllerInteraction.chatControllerNode() { let selectionColor: UIColor let knobColor: UIColor if item.message.effectivelyIncoming(item.context.account.peerId) { diff --git a/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift b/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift new file mode 100644 index 0000000000..2f142ff0f0 --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift @@ -0,0 +1,1322 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import AccountContext +import SolidRoundedButtonNode +import TelegramPresentationData +import TelegramUIPreferences +import TelegramNotices +import PresentationDataUtils +import AnimationUI +import MergeLists +import MediaResources +import StickerResources +import WallpaperResources +import TooltipUI +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import ShimmerEffect +import WallpaperBackgroundNode +import QrCode +import AvatarNode + +private func closeButtonImage(theme: PresentationTheme) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(UIColor(rgb: 0x808084, alpha: 0.1).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setStrokeColor(theme.actionSheet.inputClearButtonColor.cgColor) + + context.move(to: CGPoint(x: 10.0, y: 10.0)) + context.addLine(to: CGPoint(x: 20.0, y: 20.0)) + context.strokePath() + + context.move(to: CGPoint(x: 20.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.strokePath() + }) +} + +private struct ThemeSettingsThemeEntry: Comparable, Identifiable { + let index: Int + let emoticon: String? + let emojiFile: TelegramMediaFile? + let themeReference: PresentationThemeReference? + let nightMode: Bool + var selected: Bool + let theme: PresentationTheme + let strings: PresentationStrings + let wallpaper: TelegramWallpaper? + + var stableId: Int { + return index + } + + static func ==(lhs: ThemeSettingsThemeEntry, rhs: ThemeSettingsThemeEntry) -> Bool { + if lhs.index != rhs.index { + return false + } + if lhs.emoticon != rhs.emoticon { + return false + } + + if lhs.themeReference?.index != rhs.themeReference?.index { + return false + } + if lhs.nightMode != rhs.nightMode { + return false + } + if lhs.selected != rhs.selected { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.wallpaper != rhs.wallpaper { + return false + } + return true + } + + static func <(lhs: ThemeSettingsThemeEntry, rhs: ThemeSettingsThemeEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(context: AccountContext, action: @escaping (String?) -> Void) -> ListViewItem { + return ThemeSettingsThemeIconItem(context: context, emoticon: self.emoticon, emojiFile: self.emojiFile, themeReference: self.themeReference, nightMode: self.nightMode, selected: self.selected, theme: self.theme, strings: self.strings, wallpaper: self.wallpaper, action: action) + } +} + + +private class ThemeSettingsThemeIconItem: ListViewItem { + let context: AccountContext + let emoticon: String? + let emojiFile: TelegramMediaFile? + let themeReference: PresentationThemeReference? + let nightMode: Bool + let selected: Bool + let theme: PresentationTheme + let strings: PresentationStrings + let wallpaper: TelegramWallpaper? + let action: (String?) -> Void + + public init(context: AccountContext, emoticon: String?, emojiFile: TelegramMediaFile?, themeReference: PresentationThemeReference?, nightMode: Bool, selected: Bool, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper?, action: @escaping (String?) -> Void) { + self.context = context + self.emoticon = emoticon + self.emojiFile = emojiFile + self.themeReference = themeReference + self.nightMode = nightMode + self.selected = selected + self.theme = theme + self.strings = strings + self.wallpaper = wallpaper + self.action = action + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = ThemeSettingsThemeItemIconNode() + let (nodeLayout, apply) = node.asyncLayout()(self, params) + node.insets = nodeLayout.insets + node.contentSize = nodeLayout.contentSize + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in + apply(false) + }) + }) + } + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + assert(node() is ThemeSettingsThemeItemIconNode) + if let nodeValue = node() as? ThemeSettingsThemeItemIconNode { + let layout = nodeValue.asyncLayout() + async { + let (nodeLayout, apply) = layout(self, params) + Queue.mainQueue().async { + completion(nodeLayout, { _ in + apply(animation.isAnimated) + }) + } + } + } + } + } + + public var selectable = true + public func selected(listView: ListView) { + self.action(self.emoticon) + } +} + +private struct ThemeSettingsThemeItemNodeTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let crossfade: Bool + let entries: [ThemeSettingsThemeEntry] +} + +private func ensureThemeVisible(listNode: ListView, emoticon: String?, animated: Bool) -> Bool { + var resultNode: ThemeSettingsThemeItemIconNode? + var previousNode: ThemeSettingsThemeItemIconNode? + var nextNode: ThemeSettingsThemeItemIconNode? + listNode.forEachItemNode { node in + guard let node = node as? ThemeSettingsThemeItemIconNode else { + return + } + if resultNode == nil { + if node.item?.emoticon == emoticon { + resultNode = node + } else { + previousNode = node + } + } else if nextNode == nil { + nextNode = node + } + } + if let resultNode = resultNode { + var nodeToEnsure = resultNode + if case let .visible(resultVisibility) = resultNode.visibility, resultVisibility == 1.0 { + if let previousNode = previousNode, case let .visible(previousVisibility) = previousNode.visibility, previousVisibility < 0.5 { + nodeToEnsure = previousNode + } else if let nextNode = nextNode, case let .visible(nextVisibility) = nextNode.visibility, nextVisibility < 0.5 { + nodeToEnsure = nextNode + } + } + listNode.ensureItemNodeVisible(nodeToEnsure, animated: animated, overflow: 57.0) + return true + } else { + return false + } +} + +private func preparedTransition(context: AccountContext, action: @escaping (String?) -> Void, from fromEntries: [ThemeSettingsThemeEntry], to toEntries: [ThemeSettingsThemeEntry], crossfade: Bool) -> ThemeSettingsThemeItemNodeTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, action: action), directionHint: .Down) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, action: action), directionHint: nil) } + + return ThemeSettingsThemeItemNodeTransition(deletions: deletions, insertions: insertions, updates: updates, crossfade: crossfade, entries: toEntries) +} + +private var cachedBorderImages: [String: UIImage] = [:] +private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selected: Bool) -> UIImage? { + let key = "\(theme.list.itemBlocksBackgroundColor.hexString)_\(selected ? "s" + theme.list.itemAccentColor.hexString : theme.list.disclosureArrowColor.hexString)" + if let image = cachedBorderImages[key] { + return image + } else { + let image = generateImage(CGSize(width: 18.0, height: 18.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let lineWidth: CGFloat + if selected { + lineWidth = 2.0 + context.setLineWidth(lineWidth) + context.setStrokeColor(theme.list.itemBlocksBackgroundColor.cgColor) + + context.strokeEllipse(in: bounds.insetBy(dx: 3.0 + lineWidth / 2.0, dy: 3.0 + lineWidth / 2.0)) + + var accentColor = theme.list.itemAccentColor + if accentColor.rgb == 0xffffff { + accentColor = UIColor(rgb: 0x999999) + } + context.setStrokeColor(accentColor.cgColor) + } else { + context.setStrokeColor(theme.list.disclosureArrowColor.withAlphaComponent(0.4).cgColor) + lineWidth = 1.0 + } + + if bordered || selected { + context.setLineWidth(lineWidth) + context.strokeEllipse(in: bounds.insetBy(dx: 1.0 + lineWidth / 2.0, dy: 1.0 + lineWidth / 2.0)) + } + })?.stretchableImage(withLeftCapWidth: 9, topCapHeight: 9) + cachedBorderImages[key] = image + return image + } +} + +private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { + private let containerNode: ASDisplayNode + private let emojiContainerNode: ASDisplayNode + private let imageNode: TransformImageNode + private let overlayNode: ASImageNode + private let textNode: TextNode + private let emojiNode: TextNode + private let emojiImageNode: TransformImageNode + private var animatedStickerNode: AnimatedStickerNode? + private var placeholderNode: StickerShimmerEffectNode + var snapshotView: UIView? + + var item: ThemeSettingsThemeIconItem? + + override var visibility: ListViewItemNodeVisibility { + didSet { + self.visibilityStatus = self.visibility != .none + } + } + + private var visibilityStatus: Bool = false { + didSet { + if self.visibilityStatus != oldValue { + self.animatedStickerNode?.visibility = self.visibilityStatus + } + } + } + + private let stickerFetchedDisposable = MetaDisposable() + + init() { + self.containerNode = ASDisplayNode() + self.emojiContainerNode = ASDisplayNode() + + self.imageNode = TransformImageNode() + self.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 82.0, height: 108.0)) + self.imageNode.isLayerBacked = true + self.imageNode.cornerRadius = 8.0 + self.imageNode.clipsToBounds = true + + self.overlayNode = ASImageNode() + self.overlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 84.0, height: 110.0)) + self.overlayNode.isLayerBacked = true + + self.textNode = TextNode() + self.textNode.isUserInteractionEnabled = false + self.textNode.displaysAsynchronously = false + + self.emojiNode = TextNode() + self.emojiNode.isUserInteractionEnabled = false + self.emojiNode.displaysAsynchronously = false + + self.emojiImageNode = TransformImageNode() + + self.placeholderNode = StickerShimmerEffectNode() + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.imageNode) + self.containerNode.addSubnode(self.overlayNode) + self.containerNode.addSubnode(self.textNode) + + self.addSubnode(self.emojiContainerNode) + self.emojiContainerNode.addSubnode(self.emojiNode) + self.emojiContainerNode.addSubnode(self.emojiImageNode) + self.emojiContainerNode.addSubnode(self.placeholderNode) + + var firstTime = true + self.emojiImageNode.imageUpdated = { [weak self] image in + guard let strongSelf = self else { + return + } + if image != nil { + strongSelf.removePlaceholder(animated: !firstTime) + if firstTime { + strongSelf.emojiImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + firstTime = false + } + } + + deinit { + self.stickerFetchedDisposable.dispose() + } + + private func removePlaceholder(animated: Bool) { + if !animated { + self.placeholderNode.removeFromSupernode() + } else { + self.placeholderNode.alpha = 0.0 + self.placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in + self?.placeholderNode.removeFromSupernode() + }) + } + } + + override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + let emojiFrame = CGRect(origin: CGPoint(x: 28.0, y: 71.0), size: CGSize(width: 34.0, height: 34.0)) + self.placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + emojiFrame.minX, y: rect.minY + emojiFrame.minY), size: emojiFrame.size), within: containerSize) + } + + override func selected() { + let wasSelected = self.item?.selected ?? false + super.selected() + + if let animatedStickerNode = self.animatedStickerNode { + Queue.mainQueue().after(0.1) { + if !wasSelected { + animatedStickerNode.seekTo(.frameIndex(0)) + animatedStickerNode.play() + + let scale: CGFloat = 1.95 + animatedStickerNode.transform = CATransform3DMakeScale(scale, scale, 1.0) + animatedStickerNode.layer.animateSpring(from: 1.0 as NSNumber, to: scale as NSNumber, keyPath: "transform.scale", duration: 0.45) + + animatedStickerNode.completed = { [weak animatedStickerNode, weak self] _ in + guard let item = self?.item, item.selected else { + return + } + animatedStickerNode?.transform = CATransform3DIdentity + animatedStickerNode?.layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) + } + } + } + } + + } + + func asyncLayout() -> (ThemeSettingsThemeIconItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) { + let makeTextLayout = TextNode.asyncLayout(self.textNode) + let makeEmojiLayout = TextNode.asyncLayout(self.emojiNode) + let makeImageLayout = self.imageNode.asyncLayout() + + let currentItem = self.item + + return { [weak self] item, params in + var updatedEmoticon = false + var updatedThemeReference = false + var updatedTheme = false + var updatedWallpaper = false + var updatedSelected = false + var updatedNightMode = false + + if currentItem?.emoticon != item.emoticon { + updatedEmoticon = true + } + if currentItem?.themeReference != item.themeReference { + updatedThemeReference = true + } + if currentItem?.wallpaper != item.wallpaper { + updatedWallpaper = true + } + if currentItem?.theme !== item.theme { + updatedTheme = true + } + if currentItem?.selected != item.selected { + updatedSelected = true + } + if currentItem?.nightMode != item.nightMode { + updatedNightMode = true + } + + let text = NSAttributedString(string: item.strings.Conversation_Theme_NoTheme, font: Font.semibold(15.0), textColor: item.theme.actionSheet.controlAccentColor) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let emoticon = item.emoticon + let title = NSAttributedString(string: emoticon != nil ? "" : "❌", font: Font.regular(22.0), textColor: .black) + let (_, emojiApply) = makeEmojiLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 120.0, height: 90.0), insets: UIEdgeInsets()) + return (itemLayout, { animated in + if let strongSelf = self { + strongSelf.item = item + + if updatedThemeReference || updatedWallpaper || updatedNightMode { + if let themeReference = item.themeReference { + strongSelf.imageNode.setSignal(themeIconImage(account: item.context.account, accountManager: item.context.sharedContext.accountManager, theme: themeReference, color: nil, wallpaper: item.wallpaper, nightMode: item.nightMode, emoticon: true, qr: true)) + strongSelf.imageNode.backgroundColor = nil + } + } + if item.themeReference == nil { + strongSelf.imageNode.backgroundColor = item.theme.actionSheet.opaqueItemBackgroundColor + } + + if updatedTheme || updatedSelected { + strongSelf.overlayNode.image = generateBorderImage(theme: item.theme, bordered: false, selected: item.selected) + } + + if !item.selected && currentItem?.selected == true, let animatedStickerNode = strongSelf.animatedStickerNode { + animatedStickerNode.transform = CATransform3DIdentity + + let initialScale: CGFloat = CGFloat((animatedStickerNode.value(forKeyPath: "layer.presentationLayer.transform.scale.x") as? NSNumber)?.floatValue ?? 1.0) + animatedStickerNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) + } + + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((90.0 - textLayout.size.width) / 2.0), y: 24.0), size: textLayout.size) + strongSelf.textNode.isHidden = item.emoticon != nil + + strongSelf.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + strongSelf.containerNode.frame = CGRect(origin: CGPoint(x: 15.0, y: -15.0), size: CGSize(width: 90.0, height: 120.0)) + + strongSelf.emojiContainerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + strongSelf.emojiContainerNode.frame = CGRect(origin: CGPoint(x: 15.0, y: -15.0), size: CGSize(width: 90.0, height: 120.0)) + + let _ = textApply() + let _ = emojiApply() + + let imageSize = CGSize(width: 82.0, height: 108.0) + strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: 4.0, y: 6.0), size: imageSize) + let applyLayout = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: .clear)) + applyLayout() + + strongSelf.overlayNode.frame = strongSelf.imageNode.frame.insetBy(dx: -1.0, dy: -1.0) + strongSelf.emojiNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 79.0), size: CGSize(width: 90.0, height: 30.0)) + + let emojiFrame = CGRect(origin: CGPoint(x: 28.0, y: 71.0), size: CGSize(width: 34.0, height: 34.0)) + if let file = item.emojiFile, updatedEmoticon { + let imageApply = strongSelf.emojiImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: emojiFrame.size, boundingSize: emojiFrame.size, intrinsicInsets: UIEdgeInsets())) + imageApply() + strongSelf.emojiImageNode.setSignal(chatMessageStickerPackThumbnail(postbox: item.context.account.postbox, resource: file.resource, animated: true, nilIfEmpty: true)) + strongSelf.emojiImageNode.frame = emojiFrame + + let animatedStickerNode: AnimatedStickerNode + if let current = strongSelf.animatedStickerNode { + animatedStickerNode = current + } else { + animatedStickerNode = AnimatedStickerNode() + animatedStickerNode.started = { [weak self] in + self?.emojiImageNode.isHidden = true + } + strongSelf.animatedStickerNode = animatedStickerNode + strongSelf.emojiContainerNode.insertSubnode(animatedStickerNode, belowSubnode: strongSelf.placeholderNode) + let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 128, height: 128, playbackMode: .still(.start), mode: .direct(cachePathPrefix: pathPrefix)) + + animatedStickerNode.anchorPoint = CGPoint(x: 0.5, y: 1.0) + } + animatedStickerNode.autoplay = true + animatedStickerNode.visibility = strongSelf.visibilityStatus + + strongSelf.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) + + let thumbnailDimensions = PixelDimensions(width: 512, height: 512) + strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, imageSize: thumbnailDimensions.cgSize) + strongSelf.placeholderNode.frame = emojiFrame + } + + if let animatedStickerNode = strongSelf.animatedStickerNode { + animatedStickerNode.frame = emojiFrame + animatedStickerNode.updateLayout(size: emojiFrame.size) + } + } + }) + } + } + + func crossfade() { + if let snapshotView = self.containerNode.view.snapshotView(afterScreenUpdates: false) { + snapshotView.transform = self.containerNode.view.transform + snapshotView.frame = self.containerNode.view.frame + self.view.insertSubview(snapshotView, aboveSubview: self.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() + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + super.animateInsertion(currentTimestamp, duration: duration, short: short) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + super.animateRemoved(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + super.animateAdded(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } +} + +final class ChatQrCodeScreen: ViewController { + static let themeCrossfadeDuration: Double = 0.3 + static let themeCrossfadeDelay: Double = 0.05 + + private var controllerNode: ChatQrCodeScreenNode { + return self.displayNode as! ChatQrCodeScreenNode + } + + private var animatedIn = false + + private let context: AccountContext + private let animatedEmojiStickers: [String: [StickerPackItem]] + private let peer: Peer + + private var presentationData: PresentationData + private var presentationThemePromise = Promise() + private var presentationDataDisposable: Disposable? + + var dismissed: (() -> Void)? + + init(context: AccountContext, animatedEmojiStickers: [String: [StickerPackItem]], peer: Peer) { + self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.animatedEmojiStickers = animatedEmojiStickers + self.peer = peer + + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Ignore + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + self.blocksBackgroundWhenInOverlay = true + + self.presentationThemePromise.set(.single(nil)) + + self.presentationDataDisposable = (combineLatest(context.sharedContext.presentationData, self.presentationThemePromise.get()) + |> deliverOnMainQueue).start(next: { [weak self] presentationData, theme in + if let strongSelf = self { + var presentationData = presentationData + if let theme = theme { + presentationData = presentationData.withUpdated(theme: theme) + } + strongSelf.presentationData = presentationData + strongSelf.controllerNode.updatePresentationData(presentationData) + } + }) + + self.statusBar.statusBarStyle = .Ignore + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + override public func loadDisplayNode() { + self.displayNode = ChatQrCodeScreenNode(context: self.context, presentationData: self.presentationData, controller: self, animatedEmojiStickers: self.animatedEmojiStickers, peer: self.peer) + self.controllerNode.previewTheme = { [weak self] _, _, theme in + self?.presentationThemePromise.set(.single(theme)) + } + self.controllerNode.present = { [weak self] c in + self?.present(c, in: .current) + } + self.controllerNode.completion = { [weak self] emoticon in + guard let strongSelf = self else { + return + } + strongSelf.dismiss() + } + self.controllerNode.dismiss = { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + self.controllerNode.cancel = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.dismiss() + } + } + + override public func loadView() { + super.loadView() + + self.view.disablesInteractiveTransitionGestureRecognizer = true + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.animatedIn { + self.animatedIn = true + self.controllerNode.animateIn() + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss() + } + return true + }) + + self.controllerNode.animateOut(completion: completion) + + self.dismissed?() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + 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] { + let accentColor = theme.actionSheet.controlAccentColor + var colors: [String: UIColor] = [:] + colors["Sunny.Path 14.Path.Stroke 1"] = accentColor + colors["Sunny.Path 15.Path.Stroke 1"] = accentColor + colors["Path.Path.Stroke 1"] = accentColor + colors["Sunny.Path 39.Path.Stroke 1"] = accentColor + colors["Sunny.Path 24.Path.Stroke 1"] = accentColor + colors["Sunny.Path 25.Path.Stroke 1"] = accentColor + colors["Sunny.Path 18.Path.Stroke 1"] = accentColor + colors["Sunny.Path 41.Path.Stroke 1"] = accentColor + colors["Sunny.Path 43.Path.Stroke 1"] = accentColor + colors["Path 10.Path.Fill 1"] = accentColor + colors["Path 11.Path.Fill 1"] = accentColor + return colors +} + +private let defaultEmoticon = "🏠" + +private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDelegate { + private let context: AccountContext + 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 wrappingScrollNode: ASScrollNode + private let contentContainerNode: ASDisplayNode + private let topContentContainerNode: SparseNode + private let effectNode: ASDisplayNode + private let backgroundNode: ASDisplayNode + private let contentBackgroundNode: ASDisplayNode + private let titleNode: ASTextNode + private let cancelButton: HighlightableButtonNode + private let switchThemeButton: HighlightTrackingButtonNode + private let animationContainerNode: ASDisplayNode + private var animationNode: AnimationNode + private let doneButton: SolidRoundedButtonNode + + private let listNode: ListView + private var entries: [ThemeSettingsThemeEntry]? + private var enqueuedTransitions: [ThemeSettingsThemeItemNodeTransition] = [] + private var initialized = false + private var themes: [TelegramTheme] = [] + + private let peer: Peer + + private var selectedEmoticon: String? { + didSet { + self.selectedEmoticonPromise.set(self.selectedEmoticon) + } + } + private var selectedEmoticonPromise: ValuePromise + + private var isDarkAppearancePromise: ValuePromise + private var isDarkAppearance: Bool = false { + didSet { + self.isDarkAppearancePromise.set(self.isDarkAppearance) + } + } + + private var containerLayout: (ContainerViewLayout, CGFloat)? + + private let disposable = MetaDisposable() + + var present: ((ViewController) -> Void)? + var previewTheme: ((String?, Bool?, PresentationTheme) -> Void)? + var completion: ((String?) -> Void)? + var dismiss: (() -> Void)? + var cancel: (() -> Void)? + + init(context: AccountContext, presentationData: PresentationData, controller: ChatQrCodeScreen, animatedEmojiStickers: [String: [StickerPackItem]], 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.contentContainerNode = ASDisplayNode() + self.contentContainerNode.isOpaque = false + + self.topContentContainerNode = SparseNode() + self.topContentContainerNode.isOpaque = false + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.clipsToBounds = true + self.backgroundNode.cornerRadius = 16.0 + + self.isDarkAppearance = self.presentationData.theme.overallDarkAppearance + self.isDarkAppearancePromise = ValuePromise(self.presentationData.theme.overallDarkAppearance) + + let backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor + let textColor = self.presentationData.theme.actionSheet.primaryTextColor + let blurStyle: UIBlurEffect.Style = self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark + + self.effectNode = ASDisplayNode(viewBlock: { + return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle)) + }) + + self.contentBackgroundNode = ASDisplayNode() + self.contentBackgroundNode.backgroundColor = backgroundColor + + self.titleNode = ASTextNode() + self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.PeerInfo_QRCode_Title, font: Font.semibold(16.0), textColor: textColor) + + self.cancelButton = HighlightableButtonNode() + self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) + + self.switchThemeButton = HighlightTrackingButtonNode() + self.animationContainerNode = ASDisplayNode() + self.animationContainerNode.isUserInteractionEnabled = false + + self.animationNode = AnimationNode(animation: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme), scale: 1.0) + self.animationNode.isUserInteractionEnabled = false + + self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 52.0, cornerRadius: 11.0, gloss: false) + self.doneButton.title = self.presentationData.strings.InviteLink_QRCode_Share + + 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.backgroundNode) + self.wrappingScrollNode.addSubnode(self.contentContainerNode) + self.wrappingScrollNode.addSubnode(self.topContentContainerNode) + + self.backgroundNode.addSubnode(self.effectNode) + self.backgroundNode.addSubnode(self.contentBackgroundNode) + self.contentContainerNode.addSubnode(self.titleNode) + self.contentContainerNode.addSubnode(self.doneButton) + + self.topContentContainerNode.addSubnode(self.animationContainerNode) + self.animationContainerNode.addSubnode(self.animationNode) + self.topContentContainerNode.addSubnode(self.switchThemeButton) + self.topContentContainerNode.addSubnode(self.listNode) + self.topContentContainerNode.addSubnode(self.cancelButton) + + self.switchThemeButton.addTarget(self, action: #selector(self.switchThemePressed), forControlEvents: .touchUpInside) + self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) + self.doneButton.pressed = { [weak self] in + if let strongSelf = self { + strongSelf.doneButton.isUserInteractionEnabled = false + strongSelf.completion?(strongSelf.selectedEmoticon) + } + } + + 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 + guard let strongSelf = self else { + return + } + + 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)) + for theme in themes { + guard let emoticon = theme.emoticon else { + continue + } + 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)) + } + + 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) + } + } + + let action: (String?) -> Void = { [weak self] emoticon in + if let strongSelf = self, strongSelf.selectedEmoticon != emoticon { + strongSelf.animateCrossfade(animateIcon: true) + + var presentationTheme: PresentationTheme? + if emoticon == defaultEmoticon { + presentationTheme = makeDefaultPresentationTheme(reference: isDarkAppearance ? .night : .dayClassic, serviceBackgroundColor: nil) + } else if let theme = themes.first(where: { $0.emoticon == emoticon }) { + if let theme = makePresentationTheme(cloudTheme: theme, dark: isDarkAppearance) { + presentationTheme = theme + } + } + if let presentationTheme = presentationTheme { + strongSelf.previewTheme?(emoticon, strongSelf.isDarkAppearance, presentationTheme) + } + strongSelf.selectedEmoticon = emoticon + let _ = ensureThemeVisible(listNode: strongSelf.listNode, emoticon: emoticon, animated: true) + } + } + let previousEntries = strongSelf.entries ?? [] + let crossfade = previousEntries.count != entries.count + let transition = preparedTransition(context: strongSelf.context, action: action, from: previousEntries, to: entries, crossfade: crossfade) + strongSelf.enqueueTransition(transition) + + 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) + } + + if isFirstTime { + for theme in themes { + if let wallpaper = theme.settings?.first?.wallpaper, case let .file(file) = wallpaper { + let account = strongSelf.context.account + let accountManager = strongSelf.context.sharedContext.accountManager + let path = accountManager.mediaBox.cachedRepresentationCompletePath(file.file.resource.id, representation: CachedPreparedPatternWallpaperRepresentation()) + if !FileManager.default.fileExists(atPath: path) { + let accountFullSizeData = Signal<(Data?, Bool), NoError> { subscriber in + let accountResource = account.postbox.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedPreparedPatternWallpaperRepresentation(), complete: false, fetch: true) + + let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .media(media: .standalone(media: file.file), resource: file.file.resource)) + let fetchedFullSizeDisposable = fetchedFullSize.start() + let fullSizeDisposable = accountResource.start(next: { next in + subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) + + if next.complete, let data = try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedRead) { + accountManager.mediaBox.storeCachedResourceRepresentation(file.file.resource, representation: CachedPreparedPatternWallpaperRepresentation(), data: data) + } + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedFullSizeDisposable.dispose() + fullSizeDisposable.dispose() + } + } + let _ = accountFullSizeData.start() + } + } + } + } + })) + + self.switchThemeButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.animationNode.layer.removeAnimation(forKey: "opacity") + strongSelf.animationNode.alpha = 0.4 + } else { + strongSelf.animationNode.alpha = 1.0 + strongSelf.animationNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + 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) + } + + private func enqueueTransition(_ transition: ThemeSettingsThemeItemNodeTransition) { + self.enqueuedTransitions.append(transition) + + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + + private func dequeueTransition() { + guard let transition = self.enqueuedTransitions.first else { + return + } + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + if self.initialized && transition.crossfade { + options.insert(.AnimateCrossfade) + } + options.insert(.Synchronous) + + var scrollToItem: ListViewScrollToItem? + if !self.initialized { + scrollToItem = ListViewScrollToItem(index: 0, position: .bottom(-57.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Down) + self.initialized = true + } + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in + }) + } + + func updatePresentationData(_ presentationData: PresentationData) { + guard !self.animatedOut else { + return + } + let previousTheme = self.presentationData.theme + self.presentationData = presentationData + + self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.semibold(16.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) + + if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + + self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) + self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme)) + + if self.animationNode.isPlaying { + if let animationNode = self.animationNode.makeCopy(colors: iconColors(theme: self.presentationData.theme), progress: 0.2) { + let previousAnimationNode = self.animationNode + self.animationNode = animationNode + + animationNode.completion = { [weak previousAnimationNode] in + previousAnimationNode?.removeFromSupernode() + } + animationNode.isUserInteractionEnabled = false + animationNode.frame = previousAnimationNode.frame + previousAnimationNode.supernode?.insertSubnode(animationNode, belowSubnode: previousAnimationNode) + previousAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreen.themeCrossfadeDuration, removeOnCompletion: false) + animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } else { + self.animationNode.setAnimation(name: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme)) + } + } + + override func didLoad() { + super.didLoad() + + 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) { + self.switchThemeButton.isUserInteractionEnabled = true + } + + self.animateCrossfade(animateIcon: false) + self.animationNode.setAnimation(name: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme)) + self.animationNode.playOnce() + + let isDarkAppearance = !self.isDarkAppearance + + var presentationTheme: PresentationTheme? + if self.selectedEmoticon == defaultEmoticon { + presentationTheme = makeDefaultPresentationTheme(reference: isDarkAppearance ? .night : .dayClassic, serviceBackgroundColor: nil) + } else if let theme = self.themes.first(where: { $0.emoticon == self.selectedEmoticon }) { + if let theme = makePresentationTheme(cloudTheme: theme, dark: isDarkAppearance) { + presentationTheme = theme + } + } + if let presentationTheme = presentationTheme { + self.previewTheme?(self.selectedEmoticon, isDarkAppearance, presentationTheme) + } + + self.isDarkAppearance = isDarkAppearance + + if isDarkAppearance { + let _ = ApplicationSpecificNotice.incrementChatSpecificThemeDarkPreviewTip(accountManager: self.context.sharedContext.accountManager, count: 3, timestamp: Int32(Date().timeIntervalSince1970)).start() + } else { + let _ = ApplicationSpecificNotice.incrementChatSpecificThemeLightPreviewTip(accountManager: self.context.sharedContext.accountManager, count: 3, timestamp: Int32(Date().timeIntervalSince1970)).start() + } + } + + private func animateCrossfade(animateIcon: Bool) { + if let snapshotView = self.containerNode.view.snapshotView(afterScreenUpdates: false) { + self.wrappingScrollNode.view.insertSubview(snapshotView, aboveSubview: self.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() + }) + } + + if animateIcon, let snapshotView = self.animationNode.view.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = self.animationNode.frame + self.animationNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.animationNode.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() + }) + } + + Queue.mainQueue().after(ChatQrCodeScreen.themeCrossfadeDelay) { + if let effectView = self.effectNode.view as? UIVisualEffectView { + UIView.animate(withDuration: ChatQrCodeScreen.themeCrossfadeDuration, delay: 0.0, options: .curveLinear) { + effectView.effect = UIBlurEffect(style: self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark) + } completion: { _ in + } + } + + let previousColor = self.contentBackgroundNode.backgroundColor ?? .clear + self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor + self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: ChatQrCodeScreen.themeCrossfadeDuration) + } + + if let snapshotView = self.contentContainerNode.view.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = self.contentContainerNode.frame + self.contentContainerNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.contentContainerNode.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() + }) + } + + self.listNode.forEachVisibleItemNode { node in + if let node = node as? ThemeSettingsThemeItemIconNode { + node.crossfade() + } + } + } + + 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 + }) + } + + func animateOut(completion: (() -> Void)? = nil) { + self.animatedOut = true + + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + self.wrappingScrollNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.dismiss?() + completion?() + } + }) + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + let contentOffset = scrollView.contentOffset + let additionalTopHeight = max(0.0, -contentOffset.y) + + if additionalTopHeight >= 30.0 { + self.cancelButtonPressed() + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.containerLayout = (layout, navigationBarHeight) + + var insets = layout.insets(options: [.statusBar, .input]) + let cleanInsets = layout.insets(options: [.statusBar]) + insets.top = max(10.0, insets.top) + + let bottomInset: CGFloat = 10.0 + cleanInsets.bottom + let titleHeight: CGFloat = 54.0 + let contentHeight = titleHeight + bottomInset + 188.0 + + let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) + + let sideInset = floor((layout.size.width - width) / 2.0) + let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight)) + let contentFrame = contentContainerFrame + + var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height + 2000.0)) + 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) + transition.updateFrame(node: self.titleNode, frame: titleFrame) + + let switchThemeSize = CGSize(width: 44.0, height: 44.0) + let switchThemeFrame = CGRect(origin: CGPoint(x: 3.0, y: 6.0), size: switchThemeSize) + transition.updateFrame(node: self.switchThemeButton, frame: switchThemeFrame) + transition.updateFrame(node: self.animationContainerNode, frame: switchThemeFrame.insetBy(dx: 9.0, dy: 9.0)) + transition.updateFrame(node: self.animationNode, frame: CGRect(origin: CGPoint(), size: self.animationContainerNode.frame.size)) + + let cancelSize = CGSize(width: 44.0, height: 44.0) + let cancelFrame = CGRect(origin: CGPoint(x: contentFrame.width - cancelSize.width - 3.0, y: 6.0), size: cancelSize) + transition.updateFrame(node: self.cancelButton, frame: cancelFrame) + + let buttonInset: CGFloat = 16.0 + let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) + transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - insets.bottom - 6.0, width: contentFrame.width, height: doneButtonHeight)) + + transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) + transition.updateFrame(node: self.topContentContainerNode, frame: contentContainerFrame) + + var listInsets = UIEdgeInsets() + listInsets.top += layout.safeInsets.left + 12.0 + listInsets.bottom += layout.safeInsets.right + 12.0 + + let contentSize = CGSize(width: contentFrame.width, height: 120.0) + + self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: contentSize.height, height: contentSize.width) + 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 + 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) + 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)) + transition.updateFrame(node: self.codeForegroundDimNode, frame: CGRect(origin: CGPoint(), size: codeBackgroundFrame.size)) + + if let codeForegroundContentNode = self.codeForegroundContentNode { + codeForegroundContentNode.frame = CGRect(origin: CGPoint(x: -self.codeForegroundNode.frame.minX, y: -self.codeForegroundNode.frame.minY), size: self.wallpaperBackgroundNode.frame.size) + } + + 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() + + 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 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)) + + if let qrCodeSize = self.qrCodeSize { + let (_, cutoutFrame, _) = qrCodeCutout(size: qrCodeSize, dimensions: imageSize, scale: nil) + self.codeIconNode.updateLayout(size: cutoutFrame.size) + + 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/ChatThemeScreen.swift b/submodules/TelegramUI/Sources/ChatThemeScreen.swift index 1e4084d40e..e24a60d6f0 100644 --- a/submodules/TelegramUI/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Sources/ChatThemeScreen.swift @@ -394,6 +394,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { var updatedTheme = false var updatedWallpaper = false var updatedSelected = false + var updatedNightMode = false if currentItem?.emoticon != item.emoticon { updatedEmoticon = true @@ -410,6 +411,9 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { if currentItem?.selected != item.selected { updatedSelected = true } + if currentItem?.nightMode != item.nightMode { + updatedNightMode = true + } let text = NSAttributedString(string: item.strings.Conversation_Theme_NoTheme, font: Font.semibold(15.0), textColor: item.theme.actionSheet.controlAccentColor) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) @@ -423,7 +427,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { if let strongSelf = self { strongSelf.item = item - if updatedThemeReference || updatedWallpaper { + if updatedThemeReference || updatedWallpaper || updatedNightMode { if let themeReference = item.themeReference { strongSelf.imageNode.setSignal(themeIconImage(account: item.context.account, accountManager: item.context.sharedContext.accountManager, theme: themeReference, color: nil, wallpaper: item.wallpaper, nightMode: item.nightMode, emoticon: true)) strongSelf.imageNode.backgroundColor = nil diff --git a/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift index 15a1315d92..6100fa6f54 100644 --- a/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift @@ -169,7 +169,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { if let currentEditMediaReference = self.currentEditMediaReference { effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media]) } - (text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(effectiveMessage), strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, dateTimeFormat: self.dateTimeFormat, accountPeerId: self.context.account.peerId) + (text, _, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(effectiveMessage), strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, dateTimeFormat: self.dateTimeFormat, accountPeerId: self.context.account.peerId) } var updatedMediaReference: AnyMediaReference? diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index cac743bd4c..f77ed82440 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -5394,7 +5394,35 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate guard let data = self.data, let peer = data.peer, let controller = self.controller else { return } - controller.present(QrCodeScreen(context: self.context, updatedPresentationData: controller.updatedPresentationData, subject: .peer(peer: EnginePeer(peer))), in: .window(.root)) + + 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)) } fileprivate func openSettings(section: PeerInfoSettingsSection) { @@ -6639,7 +6667,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .done, isForExpandedView: false)) } else { if self.isSettings { - leftNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .qrCode, isForExpandedView: false)) + if let addressName = self.data?.peer?.addressName, !addressName.isEmpty { + leftNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .qrCode, isForExpandedView: false)) + } rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .edit, isForExpandedView: false)) rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .search, isForExpandedView: true)) diff --git a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift index 2cff35dd92..57550bec1e 100644 --- a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift @@ -106,7 +106,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { authorName = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder) } if let message = message { - (text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) + (text, _, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) } var updatedMediaReference: AnyMediaReference? diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index 0fad7a6334..a97a6c14f6 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -19,6 +19,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var enableDebugDataDisplay: Bool public var acceleratedStickers: Bool public var experimentalBackground: Bool + public var snow: Bool public static var defaultSettings: ExperimentalUISettings { return ExperimentalUISettings( @@ -36,7 +37,8 @@ public struct ExperimentalUISettings: Codable, Equatable { experimentalCompatibility: false, enableDebugDataDisplay: false, acceleratedStickers: false, - experimentalBackground: false + experimentalBackground: false, + snow: false ) } @@ -55,7 +57,8 @@ public struct ExperimentalUISettings: Codable, Equatable { experimentalCompatibility: Bool, enableDebugDataDisplay: Bool, acceleratedStickers: Bool, - experimentalBackground: Bool + experimentalBackground: Bool, + snow: Bool ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory @@ -72,6 +75,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.enableDebugDataDisplay = enableDebugDataDisplay self.acceleratedStickers = acceleratedStickers self.experimentalBackground = experimentalBackground + self.snow = snow } public init(from decoder: Decoder) throws { @@ -92,6 +96,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.enableDebugDataDisplay = (try container.decodeIfPresent(Int32.self, forKey: "enableDebugDataDisplay") ?? 0) != 0 self.acceleratedStickers = (try container.decodeIfPresent(Int32.self, forKey: "acceleratedStickers") ?? 0) != 0 self.experimentalBackground = (try container.decodeIfPresent(Int32.self, forKey: "experimentalBackground") ?? 0) != 0 + self.snow = (try container.decodeIfPresent(Int32.self, forKey: "snow") ?? 0) != 0 } public func encode(to encoder: Encoder) throws { @@ -112,6 +117,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encode((self.enableDebugDataDisplay ? 1 : 0) as Int32, forKey: "enableDebugDataDisplay") try container.encode((self.acceleratedStickers ? 1 : 0) as Int32, forKey: "acceleratedStickers") try container.encode((self.experimentalBackground ? 1 : 0) as Int32, forKey: "experimentalBackground") + try container.encode((self.snow ? 1 : 0) as Int32, forKey: "snow") } } diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 1a3d5022b4..14fca54731 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -698,7 +698,7 @@ public func breakChatInputText(_ text: NSAttributedString) -> [NSAttributedStrin } } -private let markdownRegexFormat = "(^|\\s|\\n)(````?)([\\s\\S]+?)(````?)([\\s\\n\\.,:?!;]|$)|(^|\\s)(`|\\*\\*|__|~~)([^\\n]+?)\\7([\\s\\.,:?!;]|$)|@(\\d+)\\s*\\((.+?)\\)" +private let markdownRegexFormat = "(^|\\s|\\n)(````?)([\\s\\S]+?)(````?)([\\s\\n\\.,:?!;]|$)|(^|\\s)(`|\\*\\*|__|~~|\\|\\|)([^\\n]+?)\\7([\\s\\.,:?!;]|$)|@(\\d+)\\s*\\((.+?)\\)" private let markdownRegex = try? NSRegularExpression(pattern: markdownRegexFormat, options: [.caseInsensitive, .anchorsMatchLines]) public func convertMarkdownToAttributes(_ text: NSAttributedString) -> NSAttributedString { diff --git a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift index 93f3111b3d..9ffa923f97 100644 --- a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift +++ b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift @@ -188,6 +188,7 @@ public enum TextSelectionAction { case share case lookup case speak + case translate } public final class TextSelectionNode: ASDisplayNode { @@ -501,6 +502,12 @@ public final class TextSelectionNode: ASDisplayNode { self?.performAction(attributedText, .lookup) self?.dismissSelection() })) +// if #available(iOS 15.0, *) { +// actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuTranslate, accessibilityLabel: self.strings.Conversation_ContextMenuTranslate), action: { [weak self] in +// self?.performAction(attributedText, .translate) +// self?.dismissSelection() +// })) +// } if isSpeakSelectionEnabled() { actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuSpeak, accessibilityLabel: self.strings.Conversation_ContextMenuSpeak), action: { [weak self] in self?.performAction(attributedText, .speak) diff --git a/submodules/Translate/BUILD b/submodules/Translate/BUILD new file mode 100644 index 0000000000..a6dcb04479 --- /dev/null +++ b/submodules/Translate/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "Translate", + module_name = "Translate", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display:Display", + "//submodules/AccountContext:AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Translate/Sources/Translate.swift b/submodules/Translate/Sources/Translate.swift new file mode 100644 index 0000000000..539319d70b --- /dev/null +++ b/submodules/Translate/Sources/Translate.swift @@ -0,0 +1,27 @@ +import Foundation +import UIKit +import Display +import AccountContext + +// Incuding at least one Objective-C class in a swift file ensures that it doesn't get stripped by the linker +private final class LinkHelperClass: NSObject { +} + +public func translateText(context: AccountContext, text: String) { + guard !text.isEmpty else { + return + } + if #available(iOS 15.0, *) { + let textField = UITextField() + textField.text = text + if let navigationController = context.sharedContext.mainWindow?.viewController as? NavigationController, let topController = navigationController.topViewController as? ViewController { + topController.view.addSubview(textField) + textField.selectAll(nil) + textField.perform(NSSelectorFromString(["_", "trans", "late:"].joined(separator: "")), with: nil) + + DispatchQueue.main.async { + textField.removeFromSuperview() + } + } + } +} diff --git a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift index 1297e5144a..8105368973 100644 --- a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift +++ b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift @@ -56,6 +56,8 @@ public protocol WallpaperBackgroundNode: ASDisplayNode { func updateBubbleTheme(bubbleTheme: PresentationTheme, bubbleCorners: PresentationChatBubbleCorners) func hasBubbleBackground(for type: WallpaperBubbleType) -> Bool func makeBubbleBackground(for type: WallpaperBubbleType) -> WallpaperBubbleBackgroundNode? + + func makeDimmedNode() -> ASDisplayNode? } final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode { @@ -799,7 +801,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode if isFirstLayout && !self.frame.isEmpty { self.updateScale() - if false, self.newYearNode == nil { + if self.context.sharedContext.immediateExperimentalUISettings.snow, self.newYearNode == nil { let newYearNode = WallpaperNewYearNode() self.addSubnode(newYearNode) self.newYearNode = newYearNode @@ -897,6 +899,14 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode node.updateContents() return node } + + func makeDimmedNode() -> ASDisplayNode? { + if let gradientBackgroundNode = self.gradientBackgroundNode { + return GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode) + } else { + return nil + } + } } private protocol WallpaperComponentView: AnyObject { @@ -1695,6 +1705,10 @@ final class WallpaperBackgroundNodeMergedImpl: ASDisplayNode, WallpaperBackgroun node.updateContents() return node } + + func makeDimmedNode() -> ASDisplayNode? { + return nil + } } private let sharedStorage = WallpaperBackgroundNodeMergedImpl.SharedStorage() @@ -1738,7 +1752,7 @@ private class WallpaperNewYearNode: ASDisplayNode { cell1.scale = 0.04 cell1.scaleRange = 0.15 cell1.color = UIColor.white.withAlphaComponent(0.88).cgColor - cell1.alphaRange = -0.2 +// cell1.alphaRange = -0.2 particlesLayer.emitterCells = [cell1] } diff --git a/submodules/WallpaperResources/Sources/WallpaperResources.swift b/submodules/WallpaperResources/Sources/WallpaperResources.swift index b6e7549af1..0f1479172e 100644 --- a/submodules/WallpaperResources/Sources/WallpaperResources.swift +++ b/submodules/WallpaperResources/Sources/WallpaperResources.swift @@ -1300,7 +1300,23 @@ public func themeImage(account: Account, accountManager: AccountManager, theme: PresentationThemeReference, color: PresentationThemeAccentColor?, wallpaper: TelegramWallpaper? = nil, nightMode: Bool? = nil, emoticon: Bool = false, large: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +private let qrIconImage: UIImage = { + return generateImage(CGSize(width: 36.0, height: 36.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setFillColor(UIColor.white.cgColor) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 9.0).cgPath) + context.fillPath() + + if let image = UIImage(bundleImageName: "Settings/QrButtonIcon")?.cgImage { + context.clip(to: CGRect(x: 6.0, y: 6.0, width: 24.0, height: 24.0), mask: image) + context.clear(bounds) + } + })! +}() + +public func themeIconImage(account: Account, accountManager: AccountManager, theme: PresentationThemeReference, color: PresentationThemeAccentColor?, wallpaper: TelegramWallpaper? = nil, nightMode: Bool? = nil, emoticon: Bool = false, large: Bool = false, qr: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let colorsSignal: Signal<((UIColor, UIColor?, [UInt32]), [UIColor], [UIColor], UIImage?, Bool, Bool, CGFloat, Int32?), NoError> var reference: MediaResourceReference? @@ -1552,134 +1568,140 @@ public func themeIconImage(account: Account, accountManager: AccountManager 1 { - c.clip() + c.translateBy(x: 7.0, y: 27.0) + c.translateBy(x: 114.0, y: 32.0) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: -114.0, y: -32.0) + + let _ = try? drawSvgPath(c, path: "M12.8304,29.8712 C10.0551,31.8416 6.6628,33 2.99998,33 C1.98426,33 0.989361,32.9109 0.022644,32.7402 C2.97318,31.9699 5.24596,29.5785 5.84625,26.5607 C5.99996,25.7879 5.99996,24.8586 5.99996,23 V16.0 H6.00743 C6.27176,7.11861 13.5546,0 22.5,0 H61.5 C70.6127,0 78,7.3873 78,16.5 C78,25.6127 70.6127,33 61.5,33 H22.5 C18.8883,33 15.5476,31.8396 12.8304,29.8712 ") + if Set(incomingColors.map(\.rgb)).count > 1 { + c.clip() - var colors: [CGColor] = [] - var locations: [CGFloat] = [] - for i in 0 ..< incomingColors.count { - let t = CGFloat(i) / CGFloat(incomingColors.count - 1) - locations.append(t) - colors.append(incomingColors[i].cgColor) + var colors: [CGColor] = [] + var locations: [CGFloat] = [] + for i in 0 ..< incomingColors.count { + let t = CGFloat(i) / CGFloat(incomingColors.count - 1) + locations.append(t) + colors.append(incomingColors[i].cgColor) + } + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as NSArray, locations: &locations)! + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 34.0), options: CGGradientDrawingOptions()) + } else { + c.setFillColor(incomingColors[0].cgColor) + c.fillPath() } - - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as NSArray, locations: &locations)! - c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 34.0), options: CGGradientDrawingOptions()) + + c.restoreGState() } else { - c.setFillColor(incomingColors[0].cgColor) - c.fillPath() - } - - c.restoreGState() - } else { - let rect = CGRect(x: 8.0, y: 44.0, width: 48.0, height: 24.0) - c.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 12.0).cgPath) - c.clip() - - if incomingColors.count >= 2 { - let gradientColors = incomingColors.reversed().map { $0.cgColor } as CFArray + let rect = CGRect(x: 8.0, y: 44.0, width: 48.0, height: 24.0) + c.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 12.0).cgPath) + c.clip() + + if incomingColors.count >= 2 { + let gradientColors = incomingColors.reversed().map { $0.cgColor } as CFArray - var locations: [CGFloat] = [] - for i in 0 ..< incomingColors.count { - let t = CGFloat(i) / CGFloat(incomingColors.count - 1) - locations.append(t) + var locations: [CGFloat] = [] + for i in 0 ..< incomingColors.count { + let t = CGFloat(i) / CGFloat(incomingColors.count - 1) + locations.append(t) + } + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: rect.minY), end: CGPoint(x: 0.0, y: rect.maxY), options: CGGradientDrawingOptions()) + } else if !incomingColors.isEmpty { + c.setFillColor(incomingColors[0].cgColor) + c.fill(rect) } - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + c.resetClip() + } + } else { + let incoming = generateGradientTintedImage(image: UIImage(bundleImageName: "Settings/ThemeBubble"), colors: incomingColors) + c.draw(incoming!.cgImage!, in: CGRect(x: 9.0, y: 34.0, width: 57.0, height: 16.0)) + } + + if !(emoticon && large) { + c.translateBy(x: drawingRect.width / 2.0, y: drawingRect.height / 2.0) + c.scaleBy(x: -1.0, y: 1.0) + c.translateBy(x: -drawingRect.width / 2.0, y: -drawingRect.height / 2.0) + } + + let outgoingColors = colors.2 + if emoticon { + if large { + c.saveGState() + + c.translateBy(x: (drawingRect.width - 120) - 71, y: 66.0) + c.translateBy(x: 114.0, y: 32.0) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: 0.0, y: -32.0) + + let _ = try? drawSvgPath(c, path: "M57.1696,29.8712 C59.9449,31.8416 63.3372,33 67,33 C68.0157,33 69.0106,32.9109 69.9773,32.7402 C67.0268,31.9699 64.754,29.5786 64.1537,26.5607 C64,25.7879 64,24.8586 64,23 V16.5 V16 H63.9926 C63.7282,7.11861 56.4454,0 47.5,0 H16.5 C7.3873,0 0,7.3873 0,16.5 C0,25.6127 7.3873,33 16.5,33 H47.5 C51.1117,33 54.4524,31.8396 57.1696,29.8712 ") + if Set(outgoingColors.map(\.rgb)).count > 1 { + c.clip() - c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: rect.minY), end: CGPoint(x: 0.0, y: rect.maxY), options: CGGradientDrawingOptions()) - } else if !incomingColors.isEmpty { - c.setFillColor(incomingColors[0].cgColor) - c.fill(rect) + var colors: [CGColor] = [] + var locations: [CGFloat] = [] + for i in 0 ..< outgoingColors.count { + let t = CGFloat(i) / CGFloat(outgoingColors.count - 1) + locations.append(t) + colors.append(outgoingColors[i].cgColor) + } + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as NSArray, locations: &locations)! + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 34.0), options: CGGradientDrawingOptions()) + } else { + c.setFillColor(outgoingColors[0].cgColor) + c.fillPath() + } + + c.restoreGState() + } else { + let rect = CGRect(x: 8.0, y: 72.0, width: 48.0, height: 24.0) + c.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 12.0).cgPath) + c.clip() + + if outgoingColors.count >= 2 { + let gradientColors = outgoingColors.reversed().map { $0.cgColor } as CFArray + + var locations: [CGFloat] = [] + for i in 0 ..< outgoingColors.count { + let t = CGFloat(i) / CGFloat(outgoingColors.count - 1) + locations.append(t) + } + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: rect.minY), end: CGPoint(x: 0.0, y: rect.maxY), options: CGGradientDrawingOptions()) + } else if !outgoingColors.isEmpty { + c.setFillColor(outgoingColors[0].cgColor) + c.fill(rect) + } } c.resetClip() - } - } else { - let incoming = generateGradientTintedImage(image: UIImage(bundleImageName: "Settings/ThemeBubble"), colors: incomingColors) - c.draw(incoming!.cgImage!, in: CGRect(x: 9.0, y: 34.0, width: 57.0, height: 16.0)) - } - - if !(emoticon && large) { - c.translateBy(x: drawingRect.width / 2.0, y: drawingRect.height / 2.0) - c.scaleBy(x: -1.0, y: 1.0) - c.translateBy(x: -drawingRect.width / 2.0, y: -drawingRect.height / 2.0) - } - - let outgoingColors = colors.2 - if emoticon { - if large { - c.saveGState() - - c.translateBy(x: (drawingRect.width - 120) - 71, y: 66.0) - c.translateBy(x: 114.0, y: 32.0) - c.scaleBy(x: 1.0, y: -1.0) - c.translateBy(x: 0.0, y: -32.0) - - let _ = try? drawSvgPath(c, path: "M57.1696,29.8712 C59.9449,31.8416 63.3372,33 67,33 C68.0157,33 69.0106,32.9109 69.9773,32.7402 C67.0268,31.9699 64.754,29.5786 64.1537,26.5607 C64,25.7879 64,24.8586 64,23 V16.5 V16 H63.9926 C63.7282,7.11861 56.4454,0 47.5,0 H16.5 C7.3873,0 0,7.3873 0,16.5 C0,25.6127 7.3873,33 16.5,33 H47.5 C51.1117,33 54.4524,31.8396 57.1696,29.8712 ") - if Set(outgoingColors.map(\.rgb)).count > 1 { - c.clip() - - var colors: [CGColor] = [] - var locations: [CGFloat] = [] - for i in 0 ..< outgoingColors.count { - let t = CGFloat(i) / CGFloat(outgoingColors.count - 1) - locations.append(t) - colors.append(outgoingColors[i].cgColor) - } - - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as NSArray, locations: &locations)! - c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 34.0), options: CGGradientDrawingOptions()) - } else { - c.setFillColor(outgoingColors[0].cgColor) - c.fillPath() - } - - c.restoreGState() } else { - let rect = CGRect(x: 8.0, y: 72.0, width: 48.0, height: 24.0) - c.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 12.0).cgPath) - c.clip() - - if outgoingColors.count >= 2 { - let gradientColors = outgoingColors.reversed().map { $0.cgColor } as CFArray - - var locations: [CGFloat] = [] - for i in 0 ..< outgoingColors.count { - let t = CGFloat(i) / CGFloat(outgoingColors.count - 1) - locations.append(t) - } - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! - - c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: rect.minY), end: CGPoint(x: 0.0, y: rect.maxY), options: CGGradientDrawingOptions()) - } else if !outgoingColors.isEmpty { - c.setFillColor(outgoingColors[0].cgColor) - c.fill(rect) - } + let outgoing = generateGradientTintedImage(image: UIImage(bundleImageName: "Settings/ThemeBubble"), colors: outgoingColors) + c.draw(outgoing!.cgImage!, in: CGRect(x: 9.0, y: 12.0, width: 57.0, height: 16.0)) } - - c.resetClip() - } else { - let outgoing = generateGradientTintedImage(image: UIImage(bundleImageName: "Settings/ThemeBubble"), colors: outgoingColors) - c.draw(outgoing!.cgImage!, in: CGRect(x: 9.0, y: 12.0, width: 57.0, height: 16.0)) } } addCorners(context, arguments: arguments)