From d9f3dba2924451207e8a88dcd26dd1fc975a011f Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 16 Apr 2021 14:22:38 +0300 Subject: [PATCH] Add external sticker set import --- .../Sources/AccountContext.swift | 1 + submodules/ImportStickerPackUI/BUILD | 35 ++ .../Sources/ImportStickerPack.swift | 49 ++ .../Sources/ImportStickerPackController.swift | 107 ++++ .../ImportStickerPackControllerNode.swift | 554 ++++++++++++++++++ .../Sources/StickerPackPreviewGridItem.swift | 255 ++++++++ .../Sources/StickerPreviewPeekContent.swift | 142 +++++ .../Sources/InstantPageSubContentNode.swift | 447 ++++++++++++++ .../Sources/VoiceChatController.swift | 4 +- .../Sources/VoiceChatOptionsButton.swift | 38 +- .../Sources/StickerPackCreation.swift | 1 + .../PresentationResourcesSettings.swift | 2 +- .../Sources/ServiceMessageStrings.swift | 10 +- submodules/TelegramUI/BUILD | 1 + .../Settings/MenuIcons/Contents.json | 6 +- .../MenuIcons/Tips.imageset/Contents.json | 12 + .../MenuIcons/Tips.imageset/tips_30.pdf | 113 ++++ .../MenuIcons/Wallet.imageset/Contents.json | 12 - .../MenuIcons/Wallet.imageset/ic_wallet.pdf | Bin 4511 -> 0 bytes .../ChatRecentActionsControllerNode.swift | 3 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 12 + submodules/TelegramUI/Sources/OpenUrl.swift | 4 +- 22 files changed, 1786 insertions(+), 22 deletions(-) create mode 100644 submodules/ImportStickerPackUI/BUILD create mode 100644 submodules/ImportStickerPackUI/Sources/ImportStickerPack.swift create mode 100644 submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift create mode 100644 submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift create mode 100644 submodules/ImportStickerPackUI/Sources/StickerPackPreviewGridItem.swift create mode 100644 submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift create mode 100644 submodules/InstantPageUI/Sources/InstantPageSubContentNode.swift create mode 100644 submodules/TelegramCore/Sources/StickerPackCreation.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Tips.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Tips.imageset/tips_30.pdf delete mode 100644 submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Wallet.imageset/Contents.json delete mode 100644 submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Wallet.imageset/ic_wallet.pdf diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 1b3bd875fe..403c739bde 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -186,6 +186,7 @@ public enum ResolvedUrl { #endif case settings(ResolvedUrlSettingsSection) case joinVoiceChat(PeerId, String?) + case importStickers } public enum NavigateToChatKeepStack { diff --git a/submodules/ImportStickerPackUI/BUILD b/submodules/ImportStickerPackUI/BUILD new file mode 100644 index 0000000000..c515f3d26b --- /dev/null +++ b/submodules/ImportStickerPackUI/BUILD @@ -0,0 +1,35 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ImportStickerPackUI", + module_name = "ImportStickerPackUI", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/SyncCore:SyncCore", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/AccountContext:AccountContext", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/ShareController:ShareController", + "//submodules/ItemListUI:ItemListUI", + "//submodules/StickerResources:StickerResources", + "//submodules/AlertUI:AlertUI", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/TextFormat:TextFormat", + "//submodules/MergeLists:MergeLists", + "//submodules/ActivityIndicator:ActivityIndicator", + "//submodules/AnimatedStickerNode:AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", + "//submodules/ShimmerEffect:ShimmerEffect", + "//submodules/UndoUI:UndoUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPack.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPack.swift new file mode 100644 index 0000000000..6be78f7964 --- /dev/null +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPack.swift @@ -0,0 +1,49 @@ +import Foundation +import UIKit + +public class ImportStickerPack { + public class Sticker: Equatable { + public static func == (lhs: ImportStickerPack.Sticker, rhs: ImportStickerPack.Sticker) -> Bool { + return lhs.uuid == rhs.uuid + } + + let image: UIImage + let emojis: [String] + let uuid: UUID + + init(image: UIImage, emojis: [String], uuid: UUID = UUID()) { + self.image = image + self.emojis = emojis + self.uuid = uuid + } + } + + public var identifier: String + public var name: String + public let software: String + public var thumbnail: String? + public let isAnimated: Bool + + public var stickers: [Sticker] + + public init?(data: Data) { + guard let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { + return nil + } + + self.name = json["name"] as? String ?? "" + self.identifier = json["identifier"] as? String ?? "" + self.software = json["software"] as? String ?? "" + self.isAnimated = json["isAnimated"] as? Bool ?? false + + var stickers: [Sticker] = [] + if let stickersArray = json["stickers"] as? [[String: Any]] { + for sticker in stickersArray { + if let dataString = sticker["data"] as? String, let data = Data(base64Encoded: dataString), let image = UIImage(data: data) { + stickers.append(Sticker(image: image, emojis: sticker["emojis"] as? [String] ?? [])) + } + } + } + self.stickers = stickers + } +} diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift new file mode 100644 index 0000000000..4720502686 --- /dev/null +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift @@ -0,0 +1,107 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SyncCore +import SwiftSignalKit +import TelegramUIPreferences +import AccountContext +import ShareController +import StickerResources +import AlertUI +import PresentationDataUtils +import UndoUI + +public final class ImportStickerPackController: ViewController, StandalonePresentableController { + private var controllerNode: ImportStickerPackControllerNode { + return self.displayNode as! ImportStickerPackControllerNode + } + + private var animatedIn = false + private var isDismissed = false + + public var dismissed: (() -> Void)? + + private let context: AccountContext + private weak var parentNavigationController: NavigationController? + + private let stickerPack: ImportStickerPack + private var presentationDataDisposable: Disposable? + + + public init(context: AccountContext, stickerPack: ImportStickerPack, parentNavigationController: NavigationController?) { + self.context = context + self.parentNavigationController = parentNavigationController + + self.stickerPack = stickerPack + + super.init(navigationBarPresentationData: nil) + + self.blocksBackgroundWhenInOverlay = true + self.acceptsFocusWhenInOverlay = true + self.statusBar.statusBarStyle = .Ignore + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self, strongSelf.isNodeLoaded { + strongSelf.controllerNode.updatePresentationData(presentationData) + } + }) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + override public func loadDisplayNode() { + self.displayNode = ImportStickerPackControllerNode(context: self.context) + self.controllerNode.dismiss = { [weak self] in + self?.dismissed?() + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + self.controllerNode.cancel = { [weak self] in + self?.dismiss() + } + self.controllerNode.presentInGlobalOverlay = { [weak self] controller, arguments in + self?.presentInGlobalOverlay(controller, with: arguments) + } + + Queue.mainQueue().after(0.1) { + self.controllerNode.updateStickerPack(self.stickerPack) + } + self.ready.set(self.controllerNode.ready.get()) + } + + 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) { + if !self.isDismissed { + self.isDismissed = true + } else { + return + } + self.acceptsFocusWhenInOverlay = false + self.requestUpdateParameters() + self.controllerNode.animateOut(completion: completion) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } +} + diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift new file mode 100644 index 0000000000..6a50c2459d --- /dev/null +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift @@ -0,0 +1,554 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import SyncCore +import TelegramPresentationData +import TelegramUIPreferences +import MergeLists +import ActivityIndicator +import TextFormat +import AccountContext + +private struct StickerPackPreviewGridEntry: Comparable, Identifiable { + let index: Int + let stickerItem: ImportStickerPack.Sticker + + var stableId: Int { + return self.index +// return self.stickerItem.file.fileId + } + + static func <(lhs: StickerPackPreviewGridEntry, rhs: StickerPackPreviewGridEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(account: Account, interaction: StickerPackPreviewInteraction, theme: PresentationTheme) -> StickerPackPreviewGridItem { + return StickerPackPreviewGridItem(account: account, stickerItem: self.stickerItem, interaction: interaction, theme: theme, isEmpty: false) + } +} + +private struct StickerPackPreviewGridTransaction { + let deletions: [Int] + let insertions: [GridNodeInsertItem] + let updates: [GridNodeUpdateItem] + + init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], account: Account, interaction: StickerPackPreviewInteraction, theme: PresentationTheme) { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list) + + self.deletions = deleteIndices + self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction, theme: theme), previousIndex: $0.2) } + self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction, theme: theme)) } + } +} + +final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { + private let context: AccountContext + private var presentationData: PresentationData + private var stickerPack: ImportStickerPack? + + private var containerLayout: (ContainerViewLayout, CGFloat)? + + private let dimNode: ASDisplayNode + + private let wrappingScrollNode: ASScrollNode + private let cancelButtonNode: ASButtonNode + + private let contentContainerNode: ASDisplayNode + private let contentBackgroundNode: ASImageNode + private let contentGridNode: GridNode + private let installActionButtonNode: ASButtonNode + private let installActionSeparatorNode: ASDisplayNode + private let contentTitleNode: ImmediateTextNode + private let contentSeparatorNode: ASDisplayNode + + private var interaction: StickerPackPreviewInteraction! + + var presentInGlobalOverlay: ((ViewController, Any?) -> Void)? + var dismiss: (() -> Void)? + var cancel: (() -> Void)? + + let ready = Promise() + private var didSetReady = false + + private var currentItems: [StickerPackPreviewGridEntry] = [] + + private var hapticFeedback: HapticFeedback? + + init(context: AccountContext) { + self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + self.wrappingScrollNode = ASScrollNode() + self.wrappingScrollNode.view.alwaysBounceVertical = true + self.wrappingScrollNode.view.delaysContentTouches = false + self.wrappingScrollNode.view.canCancelContentTouches = true + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + + self.cancelButtonNode = ASButtonNode() + self.cancelButtonNode.displaysAsynchronously = false + + self.contentContainerNode = ASDisplayNode() + self.contentContainerNode.isOpaque = false + self.contentContainerNode.clipsToBounds = true + + self.contentBackgroundNode = ASImageNode() + self.contentBackgroundNode.displaysAsynchronously = false + self.contentBackgroundNode.displayWithoutProcessing = true + + self.contentGridNode = GridNode() + + self.installActionButtonNode = HighlightTrackingButtonNode() + self.installActionButtonNode.displaysAsynchronously = false + self.installActionButtonNode.titleNode.displaysAsynchronously = false + + self.contentTitleNode = ImmediateTextNode() + self.contentTitleNode.displaysAsynchronously = false + self.contentTitleNode.maximumNumberOfLines = 1 + + self.contentSeparatorNode = ASDisplayNode() + self.contentSeparatorNode.isLayerBacked = true + + self.installActionSeparatorNode = ASDisplayNode() + self.installActionSeparatorNode.isLayerBacked = true + self.installActionSeparatorNode.displaysAsynchronously = false + + super.init() + + self.interaction = StickerPackPreviewInteraction(playAnimatedStickers: false) + + self.backgroundColor = nil + self.isOpaque = false + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + self.addSubnode(self.dimNode) + + self.wrappingScrollNode.view.delegate = self + self.addSubnode(self.wrappingScrollNode) + + self.wrappingScrollNode.addSubnode(self.cancelButtonNode) + self.cancelButtonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) + + self.installActionButtonNode.addTarget(self, action: #selector(self.installActionButtonPressed), forControlEvents: .touchUpInside) + + self.wrappingScrollNode.addSubnode(self.contentBackgroundNode) + + self.wrappingScrollNode.addSubnode(self.contentContainerNode) + self.contentContainerNode.addSubnode(self.contentGridNode) + self.contentContainerNode.addSubnode(self.installActionSeparatorNode) + self.contentContainerNode.addSubnode(self.installActionButtonNode) + self.wrappingScrollNode.addSubnode(self.contentTitleNode) + self.wrappingScrollNode.addSubnode(self.contentSeparatorNode) + + self.contentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in + self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition) + } + } + + override func didLoad() { + super.didLoad() + + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never + } + self.contentGridNode.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>? in + if let strongSelf = self { + if let itemNode = strongSelf.contentGridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode, let item = itemNode.stickerPackItem { +// var menuItems: [PeekControllerMenuItem] = [] +// if let stickerPack = strongSelf.stickerPack, case let .result(info, _, _) = stickerPack, info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { +// if strongSelf.sendSticker != nil { +// menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, font: .bold, action: { node, rect in +// if let strongSelf = self { +// return strongSelf.sendSticker?(.standalone(media: item.file), node, rect) ?? false +// } else { +// return false +// } +// })) +// } +// menuItems.append(PeekControllerMenuItem(title: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in +// if let strongSelf = self { +// if isStarred { +// let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() +// } else { +// let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() +// } +// } +// return true +// })) +// menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true })) +// } +// return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems)) + return .single((itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: item, menu: []))) + } + } + return nil + }, present: { [weak self] content, sourceNode in + if let strongSelf = self { + let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.presentationData.theme), content: content, sourceNode: { + return sourceNode + }) + controller.visibilityUpdated = { [weak self] visible in + if let strongSelf = self { + strongSelf.contentGridNode.forceHidden = visible + } + } + strongSelf.presentInGlobalOverlay?(controller, nil) + return controller + } + return nil + }, updateContent: { [weak self] content in + if let strongSelf = self { + var item: ImportStickerPack.Sticker? + if let content = content as? StickerPreviewPeekContent { + item = content.item + } + strongSelf.updatePreviewingItem(item: item, animated: true) + } + }, activateBySingleTap: true)) + + self.updatePresentationData(self.presentationData) + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + + let theme = presentationData.theme + let solidBackground = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) + })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) + + let highlightedSolidBackground = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.actionSheet.opaqueItemHighlightedBackgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) + })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) + + let halfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0))) + })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) + + let highlightedHalfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.actionSheet.opaqueItemHighlightedBackgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0))) + })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) + + let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: presentationData.theme.actionSheet.opaqueItemBackgroundColor) + let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor) + + self.contentBackgroundNode.image = roundedBackground + + self.cancelButtonNode.setBackgroundImage(roundedBackground, for: .normal) + self.cancelButtonNode.setBackgroundImage(highlightedRoundedBackground, for: .highlighted) + + self.installActionButtonNode.setBackgroundImage(halfRoundedBackground, for: .normal) + self.installActionButtonNode.setBackgroundImage(highlightedHalfRoundedBackground, for: .highlighted) + + self.contentSeparatorNode.backgroundColor = presentationData.theme.actionSheet.opaqueItemSeparatorColor + self.installActionSeparatorNode.backgroundColor = presentationData.theme.actionSheet.opaqueItemSeparatorColor + + self.cancelButtonNode.setTitle(presentationData.strings.Common_Cancel, with: Font.medium(20.0), with: presentationData.theme.actionSheet.standardActionTextColor, for: .normal) + + self.contentTitleNode.linkHighlightColor = presentationData.theme.actionSheet.controlAccentColor.withAlphaComponent(0.5) + + if let (layout, navigationBarHeight) = self.containerLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.containerLayout = (layout, navigationBarHeight) + + transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + var insets = layout.insets(options: [.statusBar]) + insets.top = max(10.0, insets.top) + let cleanInsets = layout.insets(options: [.statusBar]) + + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + var bottomInset: CGFloat = 10.0 + cleanInsets.bottom + if insets.bottom > 0 { + bottomInset -= 12.0 + } + + let buttonHeight: CGFloat = 57.0 + let sectionSpacing: CGFloat = 8.0 + let titleAreaHeight: CGFloat = 51.0 + + let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 10.0 + layout.safeInsets.left) + + let sideInset = floor((layout.size.width - width) / 2.0) + + transition.updateFrame(node: self.cancelButtonNode, frame: CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: width, height: buttonHeight))) + + let maximumContentHeight = layout.size.height - insets.top - bottomInset - buttonHeight - sectionSpacing + + let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight)) + let contentFrame = contentContainerFrame.insetBy(dx: 12.0, dy: 0.0) + + var transaction: StickerPackPreviewGridTransaction? + + var itemCount = 0 + var animateIn = false + + if let stickerPack = self.stickerPack { + var updatedItems: [StickerPackPreviewGridEntry] = [] + for item in stickerPack.stickers { + updatedItems.append(StickerPackPreviewGridEntry(index: updatedItems.count, stickerItem: item)) + } + + if self.currentItems.isEmpty && !updatedItems.isEmpty { + let entities = generateTextEntities(stickerPack.name, enabledTypes: [.mention]) + let font = Font.medium(20.0) + self.contentTitleNode.attributedText = stringWithAppliedEntities(stickerPack.name, entities: entities, baseColor: self.presentationData.theme.actionSheet.primaryTextColor, linkColor: self.presentationData.theme.actionSheet.controlAccentColor, baseFont: font, linkFont: font, boldFont: font, italicFont: font, boldItalicFont: font, fixedFont: font, blockQuoteFont: font) + animateIn = true + itemCount = updatedItems.count + } + transaction = StickerPackPreviewGridTransaction(previousList: self.currentItems, list: updatedItems, account: self.context.account, interaction: self.interaction, theme: self.presentationData.theme) + self.currentItems = updatedItems + } + + let titleSize = self.contentTitleNode.updateLayout(CGSize(width: contentContainerFrame.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude)) + let titleFrame = CGRect(origin: CGPoint(x: contentContainerFrame.minX + floor((contentContainerFrame.size.width - titleSize.width) / 2.0), y: self.contentBackgroundNode.frame.minY + 15.0), size: titleSize) + let deltaTitlePosition = CGPoint(x: titleFrame.midX - self.contentTitleNode.frame.midX, y: titleFrame.midY - self.contentTitleNode.frame.midY) + self.contentTitleNode.frame = titleFrame + transition.animatePosition(node: self.contentTitleNode, from: CGPoint(x: titleFrame.midX + deltaTitlePosition.x, y: titleFrame.midY + deltaTitlePosition.y)) + + transition.updateFrame(node: self.contentTitleNode, frame: titleFrame) + transition.updateFrame(node: self.contentSeparatorNode, frame: CGRect(origin: CGPoint(x: contentContainerFrame.minX, y: self.contentBackgroundNode.frame.minY + titleAreaHeight), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel))) + + let itemsPerRow = 4 + let itemWidth = floor(contentFrame.size.width / CGFloat(itemsPerRow)) + let rowCount = itemCount / itemsPerRow + (itemCount % itemsPerRow != 0 ? 1 : 0) + + let minimallyRevealedRowCount: CGFloat = 3.5 + let initiallyRevealedRowCount = min(minimallyRevealedRowCount, CGFloat(rowCount)) + + let bottomGridInset = buttonHeight + let topInset = max(0.0, contentFrame.size.height - initiallyRevealedRowCount * itemWidth - titleAreaHeight - bottomGridInset) + + transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) + + let installButtonOffset = buttonHeight + transition.updateFrame(node: self.installActionButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - installButtonOffset), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight))) + transition.updateFrame(node: self.installActionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - installButtonOffset - UIScreenPixel), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel))) + + let gridSize = CGSize(width: contentFrame.size.width, height: max(32.0, contentFrame.size.height - titleAreaHeight)) + + self.contentGridNode.transaction(GridNodeTransaction(deleteItems: transaction?.deletions ?? [], insertItems: transaction?.insertions ?? [], updateItems: transaction?.updates ?? [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomGridInset, right: 0.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + transition.updateFrame(node: self.contentGridNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: titleAreaHeight), size: gridSize)) + + if animateIn { + self.contentGridNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.installActionButtonNode.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.installActionSeparatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + + private func gridPresentationLayoutUpdated(_ presentationLayout: GridNodeCurrentPresentationLayout, transition: ContainedViewLayoutTransition) { + if let (layout, _) = self.containerLayout { + var insets = layout.insets(options: [.statusBar]) + insets.top = max(10.0, insets.top) + let cleanInsets = layout.insets(options: [.statusBar]) + + var bottomInset: CGFloat = 10.0 + cleanInsets.bottom + if insets.bottom > 0 { + bottomInset -= 12.0 + } + + let buttonHeight: CGFloat = 57.0 + let sectionSpacing: CGFloat = 8.0 + let titleAreaHeight: CGFloat = 51.0 + + let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 10.0 + layout.safeInsets.left) + + let sideInset = floor((layout.size.width - width) / 2.0) + + let maximumContentHeight = layout.size.height - insets.top - bottomInset - buttonHeight - sectionSpacing + let contentFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight)) + + var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY - presentationLayout.contentOffset.y), size: contentFrame.size) + if backgroundFrame.minY < contentFrame.minY { + backgroundFrame.origin.y = contentFrame.minY + } + if backgroundFrame.maxY > contentFrame.maxY { + backgroundFrame.size.height += contentFrame.maxY - backgroundFrame.maxY + } + if backgroundFrame.size.height < buttonHeight + 32.0 { + backgroundFrame.origin.y -= buttonHeight + 32.0 - backgroundFrame.size.height + backgroundFrame.size.height = buttonHeight + 32.0 + } + var compactFrame = false + let backgroundDeltaY = backgroundFrame.minY - self.contentBackgroundNode.frame.minY + transition.updateFrame(node: self.contentBackgroundNode, frame: backgroundFrame) + transition.animatePositionAdditive(node: self.contentGridNode, offset: CGPoint(x: 0.0, y: -backgroundDeltaY)) + + let titleSize = self.contentTitleNode.bounds.size + let titleFrame = CGRect(origin: CGPoint(x: contentFrame.minX + floor((contentFrame.size.width - titleSize.width) / 2.0), y: backgroundFrame.minY + 15.0), size: titleSize) + transition.updateFrame(node: self.contentTitleNode, frame: titleFrame) + + transition.updateFrame(node: self.contentSeparatorNode, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: backgroundFrame.minY + titleAreaHeight), size: CGSize(width: contentFrame.size.width, height: UIScreenPixel))) + + if !compactFrame && CGFloat(0.0).isLessThanOrEqualTo(presentationLayout.contentOffset.y) { + self.contentSeparatorNode.alpha = 1.0 + } else { + self.contentSeparatorNode.alpha = 0.0 + } + } + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.cancelButtonPressed() + } + } + + @objc func cancelButtonPressed() { + self.cancel?() + } + + @objc func installActionButtonPressed() { + + } + + func animateIn() { + self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + + let dimPosition = self.dimNode.layer.position + self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + } + + func animateOut(completion: (() -> Void)? = nil) { + var dimCompleted = false + var offsetCompleted = false + + let internalCompletion: () -> Void = { [weak self] in + if let strongSelf = self, dimCompleted && offsetCompleted { + strongSelf.dismiss?() + } + completion?() + } + + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + dimCompleted = true + internalCompletion() + }) + + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + let dimPosition = self.dimNode.layer.position + self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + offsetCompleted = true + internalCompletion() + }) + } + + func updateStickerPack(_ stickerPack: ImportStickerPack) { + self.stickerPack = stickerPack + +// self.interaction.playAnimatedStickers = stickerSettings.loopAnimatedStickers + + if let _ = self.containerLayout { + self.dequeueUpdateStickerPack() + } + self.installActionButtonNode.setTitle("Create Sticker Set", with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) +// switch stickerPack { +// case .none, .fetching: +// self.installActionSeparatorNode.alpha = 0.0 +// self.shareActionSeparatorNode.alpha = 0.0 +// self.shareActionButtonNode.alpha = 0.0 +// self.installActionButtonNode.alpha = 0.0 +// self.installActionButtonNode.setTitle("", with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) +// case let .result(info, _, installed): +// if self.stickerPackInitiallyInstalled == nil { +// self.stickerPackInitiallyInstalled = installed +// } +// self.installActionSeparatorNode.alpha = 1.0 +// self.shareActionSeparatorNode.alpha = 1.0 +// self.shareActionButtonNode.alpha = 1.0 +// self.installActionButtonNode.alpha = 1.0 +// if installed { +// let text: String +// if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { +// text = self.presentationData.strings.StickerPack_RemoveStickerCount(info.count) +// } else { +// text = self.presentationData.strings.StickerPack_RemoveMaskCount(info.count) +// } +// self.installActionButtonNode.setTitle(text, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.destructiveActionTextColor, for: .normal) +// } else { +// let text: String +// if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { +// text = self.presentationData.strings.StickerPack_AddStickerCount(info.count) +// } else { +// text = self.presentationData.strings.StickerPack_AddMaskCount(info.count) +// } +// self.installActionButtonNode.setTitle(text, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) +// } +// } + } + + func dequeueUpdateStickerPack() { + if let (layout, navigationBarHeight) = self.containerLayout, let _ = self.stickerPack { + let transition: ContainedViewLayoutTransition + if self.didSetReady { + transition = .animated(duration: 0.4, curve: .spring) + } else { + transition = .immediate + } + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + + if !self.didSetReady { + self.didSetReady = true + self.ready.set(.single(true)) + } + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let result = self.installActionButtonNode.hitTest(self.installActionButtonNode.convert(point, from: self), with: event) { + return result + } + if self.bounds.contains(point) { + if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) && !self.cancelButtonNode.bounds.contains(self.convert(point, to: self.cancelButtonNode)) { + return self.dimNode.view + } + } + return super.hitTest(point, with: event) + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + let contentOffset = scrollView.contentOffset + let additionalTopHeight = max(0.0, -contentOffset.y) + + if additionalTopHeight >= 30.0 { + self.cancelButtonPressed() + } + } + + private func updatePreviewingItem(item: ImportStickerPack.Sticker?, animated: Bool) { + if self.interaction.previewedItem !== item { + self.interaction.previewedItem = item + + self.contentGridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? StickerPackPreviewGridItemNode { + itemNode.updatePreviewing(animated: animated) + } + } + } + } +} diff --git a/submodules/ImportStickerPackUI/Sources/StickerPackPreviewGridItem.swift b/submodules/ImportStickerPackUI/Sources/StickerPackPreviewGridItem.swift new file mode 100644 index 0000000000..bc6fd32bf2 --- /dev/null +++ b/submodules/ImportStickerPackUI/Sources/StickerPackPreviewGridItem.swift @@ -0,0 +1,255 @@ +import Foundation +import UIKit +import Display +import TelegramCore +import SyncCore +import SwiftSignalKit +import AsyncDisplayKit +import Postbox +import StickerResources +import AccountContext +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import TelegramPresentationData +import ShimmerEffect + +final class StickerPackPreviewInteraction { + var previewedItem: ImportStickerPack.Sticker? + var playAnimatedStickers: Bool + + init(playAnimatedStickers: Bool) { + self.playAnimatedStickers = playAnimatedStickers + } +} + +final class StickerPackPreviewGridItem: GridItem { + let account: Account + let stickerItem: ImportStickerPack.Sticker + let interaction: StickerPackPreviewInteraction + let theme: PresentationTheme + let isEmpty: Bool + + let section: GridSection? = nil + + init(account: Account, stickerItem: ImportStickerPack.Sticker, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, isEmpty: Bool) { + self.account = account + self.stickerItem = stickerItem + self.interaction = interaction + self.theme = theme + self.isEmpty = isEmpty + } + + func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { + let node = StickerPackPreviewGridItemNode() + node.setup(account: self.account, stickerItem: self.stickerItem, interaction: self.interaction, theme: self.theme, isEmpty: self.isEmpty) + return node + } + + func update(node: GridItemNode) { + guard let node = node as? StickerPackPreviewGridItemNode else { + assertionFailure() + return + } + node.setup(account: self.account, stickerItem: self.stickerItem, interaction: self.interaction, theme: self.theme, isEmpty: self.isEmpty) + } +} + +private let textFont = Font.regular(20.0) + +final class StickerPackPreviewGridItemNode: GridItemNode { + private var currentState: (Account, ImportStickerPack.Sticker?)? + private var isEmpty: Bool? +// private let imageNode: TransformImageNode + private let imageNode: ASImageNode + private var animationNode: AnimatedStickerNode? + private var placeholderNode: StickerShimmerEffectNode? + + private var theme: PresentationTheme? + + override var isVisibleInGrid: Bool { + didSet { + self.animationNode?.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true + } + } + + private var currentIsPreviewing = false + + private let stickerFetchedDisposable = MetaDisposable() + + var interaction: StickerPackPreviewInteraction? + + var selected: (() -> Void)? + + var stickerPackItem: ImportStickerPack.Sticker? { + return self.currentState?.1 + } + + override init() { + self.imageNode = ASImageNode() +// self.imageNode = TransformImageNode() +// self.imageNode.isLayerBacked = !smartInvertColorsEnabled() + self.placeholderNode = StickerShimmerEffectNode() + self.placeholderNode?.isUserInteractionEnabled = false + + super.init() + + self.addSubnode(self.imageNode) + if let placeholderNode = self.placeholderNode { +// self.addSubnode(placeholderNode) + } + + var firstTime = true +// self.imageNode.imageUpdated = { [weak self] image in +// guard let strongSelf = self else { +// return +// } +// if image != nil { +// strongSelf.removePlaceholder(animated: !firstTime) +// } +// firstTime = false +// } + } + + deinit { + self.stickerFetchedDisposable.dispose() + } + + private func removePlaceholder(animated: Bool) { + if let placeholderNode = self.placeholderNode { + self.placeholderNode = nil + if !animated { + placeholderNode.removeFromSupernode() + } else { + placeholderNode.allowsGroupOpacity = true + placeholderNode.alpha = 0.0 + placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in + placeholderNode?.removeFromSupernode() + placeholderNode?.allowsGroupOpacity = false + }) + } + } + } + + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:)))) + } + + func setup(account: Account, stickerItem: ImportStickerPack.Sticker?, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, isEmpty: Bool) { + self.interaction = interaction + self.theme = theme + + if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 !== stickerItem || self.isEmpty != isEmpty { + if let stickerItem = stickerItem { + self.imageNode.image = stickerItem.image +// if stickerItem.file.isAnimatedSticker { +// let dimensions = stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512) +// self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)))) +// +// if self.animationNode == nil { +// let animationNode = AnimatedStickerNode() +// self.animationNode = animationNode +// self.addSubnode(animationNode) +// animationNode.started = { [weak self] in +// self?.imageNode.isHidden = true +// self?.removePlaceholder(animated: false) +// } +// } +// let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)) +// self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) +// self.animationNode?.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true +// self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start()) +// } else { +// if let animationNode = self.animationNode { +// animationNode.visibility = false +// self.animationNode = nil +// animationNode.removeFromSupernode() +// } +// self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true)) +// self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: chatMessageStickerResource(file: stickerItem.file, small: true)).start()) +// } + } else { + if let placeholderNode = self.placeholderNode { + if isEmpty { + if !placeholderNode.alpha.isZero { + placeholderNode.alpha = 0.0 + placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } else { + placeholderNode.alpha = 1.0 + } + } + } + self.currentState = (account, stickerItem) + self.setNeedsLayout() + } + self.isEmpty = isEmpty + } + + override func layout() { + super.layout() + + let bounds = self.bounds + let boundsSide = min(bounds.size.width - 14.0, bounds.size.height - 14.0) + let boundingSize = CGSize(width: boundsSide, height: boundsSide) + + if let placeholderNode = self.placeholderNode { + let placeholderFrame = CGRect(origin: CGPoint(x: floor((bounds.width - boundingSize.width) / 2.0), y: floor((bounds.height - boundingSize.height) / 2.0)), size: boundingSize) + placeholderNode.frame = bounds + +// if let theme = self.theme, let (_, stickerItem) = self.currentState, let item = stickerItem { +// placeholderNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), data: item.file.immediateThumbnailData, size: placeholderFrame.size) +// } + } + + self.imageNode.frame = bounds +// if let (_, item) = self.currentState { +// if let item = item, let dimensions = item.file.dimensions?.cgSize { +// let imageSize = dimensions.aspectFitted(boundingSize) +// self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() +// self.imageNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) +// if let animationNode = self.animationNode { +// animationNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) +// animationNode.updateLayout(size: imageSize) +// } +// } +// } + } + + override func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) { + if let placeholderNode = self.placeholderNode { + placeholderNode.updateAbsoluteRect(absoluteRect, within: containerSize) + } + } + + func transitionNode() -> ASDisplayNode? { + return self + } + + @objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) { + } + + func updatePreviewing(animated: Bool) { + var isPreviewing = false + if let (_, maybeItem) = self.currentState, let interaction = self.interaction, let item = maybeItem { + isPreviewing = interaction.previewedItem === item + } + if self.currentIsPreviewing != isPreviewing { + self.currentIsPreviewing = isPreviewing + + if isPreviewing { + self.layer.sublayerTransform = CATransform3DMakeScale(0.8, 0.8, 1.0) + if animated { + self.layer.animateSpring(from: 1.0 as NSNumber, to: 0.8 as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.4) + } + } else { + self.layer.sublayerTransform = CATransform3DIdentity + if animated { + self.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.5) + } + } + } + } +} + diff --git a/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift b/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift new file mode 100644 index 0000000000..a91314ee4a --- /dev/null +++ b/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift @@ -0,0 +1,142 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SyncCore +import SwiftSignalKit +import StickerResources +import AnimatedStickerNode +import TelegramAnimatedStickerNode + +public final class StickerPreviewPeekContent: PeekControllerContent { + let account: Account + public let item: ImportStickerPack.Sticker + let menu: [PeekControllerMenuItem] + + public init(account: Account, item: ImportStickerPack.Sticker, menu: [PeekControllerMenuItem]) { + self.account = account + self.item = item + self.menu = menu + } + + public func presentation() -> PeekControllerContentPresentation { + return .freeform + } + + public func menuActivation() -> PeerkControllerMenuActivation { + return .press + } + + public func menuItems() -> [PeekControllerMenuItem] { + return self.menu + } + + public func node() -> PeekControllerContentNode & ASDisplayNode { + return StickerPreviewPeekContentNode(account: self.account, item: self.item) + } + + public func topAccessoryNode() -> ASDisplayNode? { + return nil + } + + public func isEqual(to: PeekControllerContent) -> Bool { + if let to = to as? StickerPreviewPeekContent { + return self.item === to.item + } else { + return false + } + } +} + +private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerContentNode { + private let account: Account + private let item: ImportStickerPack.Sticker + + private var textNode: ASTextNode + private var imageNode: ASImageNode + private var animationNode: AnimatedStickerNode? + + private var containerLayout: (ContainerViewLayout, CGFloat)? + + init(account: Account, item: ImportStickerPack.Sticker) { + self.account = account + self.item = item + + self.textNode = ASTextNode() + self.imageNode = ASImageNode() + self.imageNode.displaysAsynchronously = false + self.imageNode.image = item.image + + self.textNode.attributedText = NSAttributedString(string: item.emojis.joined(separator: " "), font: Font.regular(32.0), textColor: .black) + +// for case let .Sticker(text, _, _) in item.file.attributes { +// self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(32.0), textColor: .black) +// break +// } + +// if item.file.isAnimatedSticker { +// let animationNode = AnimatedStickerNode() +// self.animationNode = animationNode +// +// let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512) +// let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0)) +// +// self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil)) +// self.animationNode?.visibility = true +// self.animationNode?.addSubnode(self.textNode) +// } else { +// self.imageNode.addSubnode(self.textNode) +// self.animationNode = nil +// } + + + +// self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: false, fetched: true)) + + super.init() + + self.isUserInteractionEnabled = false + + if let animationNode = self.animationNode { + self.addSubnode(animationNode) + } else { + self.addSubnode(self.imageNode) + } + + self.addSubnode(self.textNode) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + let boundingSize = CGSize(width: 180.0, height: 180.0).fitted(size) + let imageFrame = CGRect(origin: CGPoint(), size: boundingSize) + + let textSpacing: CGFloat = 10.0 + let textSize = self.textNode.measure(CGSize(width: 100.0, height: 100.0)) + self.textNode.frame = CGRect(origin: CGPoint(x: floor((imageFrame.size.width - textSize.width) / 2.0), y: -textSize.height - textSpacing), size: textSize) + + self.imageNode.frame = imageFrame + return boundingSize + +// if let dimensitons = self.item.file.dimensions { +// let textSpacing: CGFloat = 10.0 +// let textSize = self.textNode.measure(CGSize(width: 100.0, height: 100.0)) +// +// let imageSize = dimensitons.cgSize.aspectFitted(boundingSize) +// self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() +// let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: textSize.height + textSpacing), size: imageSize) +// self.imageNode.frame = imageFrame +// if let animationNode = self.animationNode { +// animationNode.frame = imageFrame +// animationNode.updateLayout(size: imageSize) +// } +// +// self.textNode.frame = CGRect(origin: CGPoint(x: floor((imageFrame.size.width - textSize.width) / 2.0), y: -textSize.height - textSpacing), size: textSize) +// +// return CGSize(width: size.width, height: imageFrame.height + textSize.height + textSpacing) +// } else { +// return CGSize(width: size.width, height: 10.0) +// } + } +} diff --git a/submodules/InstantPageUI/Sources/InstantPageSubContentNode.swift b/submodules/InstantPageUI/Sources/InstantPageSubContentNode.swift new file mode 100644 index 0000000000..1b48fd5f8c --- /dev/null +++ b/submodules/InstantPageUI/Sources/InstantPageSubContentNode.swift @@ -0,0 +1,447 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SyncCore +import SwiftSignalKit +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext + +final class InstantPageSubContentNode : ASDisplayNode { + private let context: AccountContext + private let strings: PresentationStrings + private let nameDisplayOrder: PresentationPersonNameOrder + private let sourcePeerType: MediaAutoDownloadPeerType + private let theme: InstantPageTheme + + private let openMedia: (InstantPageMedia) -> Void + private let longPressMedia: (InstantPageMedia) -> Void + private let openPeer: (PeerId) -> Void + private let openUrl: (InstantPageUrlItem) -> Void + + var currentLayoutTiles: [InstantPageTile] = [] + var currentLayoutItemsWithNodes: [InstantPageItem] = [] + var distanceThresholdGroupCount: [Int: Int] = [:] + + var visibleTiles: [Int: InstantPageTileNode] = [:] + var visibleItemsWithNodes: [Int: InstantPageNode] = [:] + + var currentWebEmbedHeights: [Int : CGFloat] = [:] + var currentExpandedDetails: [Int : Bool]? + var currentDetailsItems: [InstantPageDetailsItem] = [] + + var requestLayoutUpdate: ((Bool) -> Void)? + + var currentLayout: InstantPageLayout + let contentSize: CGSize + let inOverlayPanel: Bool + + private var previousVisibleBounds: CGRect? + + init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, sourcePeerType: MediaAutoDownloadPeerType, theme: InstantPageTheme, items: [InstantPageItem], contentSize: CGSize, inOverlayPanel: Bool = false, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void) { + self.context = context + self.strings = strings + self.nameDisplayOrder = nameDisplayOrder + self.sourcePeerType = sourcePeerType + self.theme = theme + + self.openMedia = openMedia + self.longPressMedia = longPressMedia + self.openPeer = openPeer + self.openUrl = openUrl + + self.currentLayout = InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + self.contentSize = contentSize + self.inOverlayPanel = inOverlayPanel + + super.init() + + self.updateLayout() + } + + private func updateLayout() { + for (_, tileNode) in self.visibleTiles { + tileNode.removeFromSupernode() + } + self.visibleTiles.removeAll() + + let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: contentSize.width) + + var currentDetailsItems: [InstantPageDetailsItem] = [] + var currentLayoutItemsWithViews: [InstantPageItem] = [] + var distanceThresholdGroupCount: [Int: Int] = [:] + + var expandedDetails: [Int: Bool] = [:] + + var detailsIndex = -1 + for item in self.currentLayout.items { + if item.wantsNode { + currentLayoutItemsWithViews.append(item) + if let group = item.distanceThresholdGroup() { + let count: Int + if let currentCount = distanceThresholdGroupCount[Int(group)] { + count = currentCount + } else { + count = 0 + } + distanceThresholdGroupCount[Int(group)] = count + 1 + } + if let detailsItem = item as? InstantPageDetailsItem { + detailsIndex += 1 + expandedDetails[detailsIndex] = detailsItem.initiallyExpanded + currentDetailsItems.append(detailsItem) + } + } + } + + if self.currentExpandedDetails == nil { + self.currentExpandedDetails = expandedDetails + } + + self.currentLayoutTiles = currentLayoutTiles + self.currentLayoutItemsWithNodes = currentLayoutItemsWithViews + self.currentDetailsItems = currentDetailsItems + self.distanceThresholdGroupCount = distanceThresholdGroupCount + } + + var effectiveContentSize: CGSize { + var contentSize = self.contentSize + for item in self.currentDetailsItems { + let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded + contentSize.height += -item.frame.height + (expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight) + } + return contentSize + } + + func updateVisibleItems(visibleBounds: CGRect, animated: Bool = false) { + var visibleTileIndices = Set() + var visibleItemIndices = Set() + + self.previousVisibleBounds = visibleBounds + + var topNode: ASDisplayNode? + let topTileNode = topNode + if let scrollSubnodes = self.subnodes { + for node in scrollSubnodes.reversed() { + if let node = node as? InstantPageTileNode { + topNode = node + break + } + } + } + + var collapseOffset: CGFloat = 0.0 + let transition: ContainedViewLayoutTransition + if animated { + transition = .animated(duration: 0.3, curve: .spring) + } else { + transition = .immediate + } + + var itemIndex = -1 + var embedIndex = -1 + var detailsIndex = -1 + + for item in self.currentLayoutItemsWithNodes { + itemIndex += 1 + if item is InstantPageWebEmbedItem { + embedIndex += 1 + } + if item is InstantPageDetailsItem { + detailsIndex += 1 + } + + var itemThreshold: CGFloat = 0.0 + if let group = item.distanceThresholdGroup() { + var count: Int = 0 + if let currentCount = self.distanceThresholdGroupCount[group] { + count = currentCount + } + itemThreshold = item.distanceThresholdWithGroupCount(count) + } + + var itemFrame = item.frame.offsetBy(dx: 0.0, dy: -collapseOffset) + var thresholdedItemFrame = itemFrame + thresholdedItemFrame.origin.y -= itemThreshold + thresholdedItemFrame.size.height += itemThreshold * 2.0 + + if let detailsItem = item as? InstantPageDetailsItem, let expanded = self.currentExpandedDetails?[detailsIndex] { + let height = expanded ? self.effectiveSizeForDetails(detailsItem).height : detailsItem.titleHeight + collapseOffset += itemFrame.height - height + itemFrame = CGRect(origin: itemFrame.origin, size: CGSize(width: itemFrame.width, height: height)) + } + + if visibleBounds.intersects(thresholdedItemFrame) { + visibleItemIndices.insert(itemIndex) + + var itemNode = self.visibleItemsWithNodes[itemIndex] + if let currentItemNode = itemNode { + if !item.matchesNode(currentItemNode) { + (currentItemNode as! ASDisplayNode).removeFromSupernode() + self.visibleItemsWithNodes.removeValue(forKey: itemIndex) + itemNode = nil + } + } + + if itemNode == nil { + let itemIndex = itemIndex + let detailsIndex = detailsIndex + if let newNode = item.node(context: self.context, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, theme: theme, sourcePeerType: self.sourcePeerType, openMedia: { [weak self] media in + self?.openMedia(media) + }, longPressMedia: { [weak self] media in + self?.longPressMedia(media) + }, activatePinchPreview: nil, pinchPreviewFinished: nil, openPeer: { [weak self] peerId in + self?.openPeer(peerId) + }, openUrl: { [weak self] url in + self?.openUrl(url) + }, updateWebEmbedHeight: { _ in + }, updateDetailsExpanded: { [weak self] expanded in + self?.updateDetailsExpanded(detailsIndex, expanded) + }, currentExpandedDetails: self.currentExpandedDetails) { + newNode.frame = itemFrame + newNode.updateLayout(size: itemFrame.size, transition: transition) + if let topNode = topNode { + self.insertSubnode(newNode, aboveSubnode: topNode) + } else { + self.insertSubnode(newNode, at: 0) + } + topNode = newNode + self.visibleItemsWithNodes[itemIndex] = newNode + itemNode = newNode + + if let itemNode = itemNode as? InstantPageDetailsNode { + itemNode.requestLayoutUpdate = { [weak self] animated in + self?.requestLayoutUpdate?(animated) + } + } + } + } else { + if (itemNode as! ASDisplayNode).frame != itemFrame { + transition.updateFrame(node: (itemNode as! ASDisplayNode), frame: itemFrame) + itemNode?.updateLayout(size: itemFrame.size, transition: transition) + } + } + + if let itemNode = itemNode as? InstantPageDetailsNode { + itemNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY), animated: animated) + } + } + } + + topNode = topTileNode + + var tileIndex = -1 + for tile in self.currentLayoutTiles { + tileIndex += 1 + + let tileFrame = effectiveFrameForTile(tile) + var tileVisibleFrame = tileFrame + tileVisibleFrame.origin.y -= 400.0 + tileVisibleFrame.size.height += 400.0 * 2.0 + if tileVisibleFrame.intersects(visibleBounds) { + visibleTileIndices.insert(tileIndex) + + if self.visibleTiles[tileIndex] == nil { + let tileNode = InstantPageTileNode(tile: tile, backgroundColor: self.inOverlayPanel ? self.theme.overlayPanelColor : self.theme.pageBackgroundColor) + tileNode.frame = tileFrame + if let topNode = topNode { + self.insertSubnode(tileNode, aboveSubnode: topNode) + } else { + self.insertSubnode(tileNode, at: 0) + } + topNode = tileNode + self.visibleTiles[tileIndex] = tileNode + } else { + if visibleTiles[tileIndex]!.frame != tileFrame { + transition.updateFrame(node: self.visibleTiles[tileIndex]!, frame: tileFrame) + } + } + } + } + + var removeTileIndices: [Int] = [] + for (index, tileNode) in self.visibleTiles { + if !visibleTileIndices.contains(index) { + removeTileIndices.append(index) + tileNode.removeFromSupernode() + } + } + for index in removeTileIndices { + self.visibleTiles.removeValue(forKey: index) + } + + var removeItemIndices: [Int] = [] + for (index, itemNode) in self.visibleItemsWithNodes { + if !visibleItemIndices.contains(index) { + removeItemIndices.append(index) + (itemNode as! ASDisplayNode).removeFromSupernode() + } else { + var itemFrame = (itemNode as! ASDisplayNode).frame + let itemThreshold: CGFloat = 200.0 + itemFrame.origin.y -= itemThreshold + itemFrame.size.height += itemThreshold * 2.0 + itemNode.updateIsVisible(visibleBounds.intersects(itemFrame)) + } + } + for index in removeItemIndices { + self.visibleItemsWithNodes.removeValue(forKey: index) + } + } + + private func updateWebEmbedHeight(_ index: Int, _ height: CGFloat) { + // let currentHeight = self.currentWebEmbedHeights[index] + // if height != currentHeight { + // if let currentHeight = currentHeight, currentHeight > height { + // return + // } + // self.currentWebEmbedHeights[index] = height + // + // let signal: Signal = (.complete() |> delay(0.08, queue: Queue.mainQueue())) + // self.updateLayoutDisposable.set(signal.start(completed: { [weak self] in + // if let strongSelf = self { + // strongSelf.updateLayout() + // strongSelf.updateVisibleItems() + // } + // })) + // } + } + + func updateDetailsExpanded(_ index: Int, _ expanded: Bool, animated: Bool = true, requestLayout: Bool = true) { + if var currentExpandedDetails = self.currentExpandedDetails { + currentExpandedDetails[index] = expanded + self.currentExpandedDetails = currentExpandedDetails + } + self.requestLayoutUpdate?(animated) + } + + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + for (_, itemNode) in self.visibleItemsWithNodes { + if let transitionNode = itemNode.transitionNode(media: media) { + return transitionNode + } + } + return nil + } + + func updateHiddenMedia(media: InstantPageMedia?) { + for (_, itemNode) in self.visibleItemsWithNodes { + itemNode.updateHiddenMedia(media: media) + } + } + + func scrollableContentOffset(item: InstantPageScrollableItem) -> CGPoint { + var contentOffset = CGPoint() + for (_, itemNode) in self.visibleItemsWithNodes { + if let itemNode = itemNode as? InstantPageScrollableNode, itemNode.item === item { + contentOffset = itemNode.contentOffset + break + } + } + return contentOffset + } + + func nodeForDetailsItem(_ item: InstantPageDetailsItem) -> InstantPageDetailsNode? { + for (_, itemNode) in self.visibleItemsWithNodes { + if let detailsNode = itemNode as? InstantPageDetailsNode, detailsNode.item === item { + return detailsNode + } + } + return nil + } + + private func effectiveSizeForDetails(_ item: InstantPageDetailsItem) -> CGSize { + if let node = nodeForDetailsItem(item) { + return CGSize(width: item.frame.width, height: node.effectiveContentSize.height + item.titleHeight) + } else { + return item.frame.size + } + } + + private func effectiveFrameForTile(_ tile: InstantPageTile) -> CGRect { + let layoutOrigin = tile.frame.origin + var origin = layoutOrigin + for item in self.currentDetailsItems { + let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded + if layoutOrigin.y >= item.frame.maxY { + let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight + origin.y += height - item.frame.height + } + } + return CGRect(origin: origin, size: tile.frame.size) + } + + func effectiveFrameForItem(_ item: InstantPageItem) -> CGRect { + let layoutOrigin = item.frame.origin + var origin = layoutOrigin + + for item in self.currentDetailsItems { + let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded + if layoutOrigin.y >= item.frame.maxY { + let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight + origin.y += height - item.frame.height + } + } + + if let item = item as? InstantPageDetailsItem { + let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded + let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight + return CGRect(origin: origin, size: CGSize(width: item.frame.width, height: height)) + } else { + return CGRect(origin: origin, size: item.frame.size) + } + } + + func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? { + for item in self.currentLayout.items { + let itemFrame = self.effectiveFrameForItem(item) + if itemFrame.contains(location) { + if let item = item as? InstantPageTextItem, item.selectable { + return (item, CGPoint(x: itemFrame.minX - item.frame.minX, y: itemFrame.minY - item.frame.minY)) + } else if let item = item as? InstantPageScrollableItem { + let contentOffset = scrollableContentOffset(item: item) + if let (textItem, parentOffset) = item.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX + contentOffset.x, dy: -itemFrame.minY)) { + return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x - contentOffset.x, dy: parentOffset.y)) + } + } else if let item = item as? InstantPageDetailsItem { + for (_, itemNode) in self.visibleItemsWithNodes { + if let itemNode = itemNode as? InstantPageDetailsNode, itemNode.item === item { + if let (textItem, parentOffset) = itemNode.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX, dy: -itemFrame.minY)) { + return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x, dy: parentOffset.y)) + } + } + } + } + } + } + return nil + } + + + func tapActionAtPoint(_ point: CGPoint) -> TapLongTapOrDoubleTapGestureRecognizerAction { + for item in self.currentLayout.items { + let frame = self.effectiveFrameForItem(item) + if frame.contains(point) { + if item is InstantPagePeerReferenceItem { + return .fail + } else if item is InstantPageAudioItem { + return .fail + } else if item is InstantPageArticleItem { + return .fail + } else if item is InstantPageFeedbackItem { + return .fail + } else if let item = item as? InstantPageDetailsItem { + for (_, itemNode) in self.visibleItemsWithNodes { + if let itemNode = itemNode as? InstantPageDetailsNode, itemNode.item === item { + return itemNode.tapActionAtPoint(point.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY)) + } + } + } + break + } + } + return .waitForSingleTap + } +} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 6ab54eb15d..680a512334 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -843,6 +843,7 @@ public final class VoiceChatController: ViewController { } self.optionsButton = VoiceChatHeaderButton(context: self.context) + self.optionsButton.setContent(.more(optionsCircleImage(dark: false))) self.closeButton = VoiceChatHeaderButton(context: self.context) self.closeButton.setContent(.image(closeButtonImage(dark: false))) @@ -2566,6 +2567,7 @@ public final class VoiceChatController: ViewController { @objc private func optionsPressed() { if self.optionsButton.isUserInteractionEnabled { + self.optionsButton.play() self.optionsButton.contextAction?(self.optionsButton.containerNode, nil) } } @@ -3219,7 +3221,7 @@ public final class VoiceChatController: ViewController { self.bottomCornersNode.image = cornersImage(top: false, bottom: true, dark: isFullscreen) if !self.optionsButtonIsAvatar { - self.optionsButton.setContent(.image(optionsButtonImage(dark: isFullscreen)), animated: transition.isAnimated) + self.optionsButton.setContent(.more(optionsCircleImage(dark: isFullscreen)), animated: transition.isAnimated) } self.closeButton.setContent(.image(closeButtonImage(dark: isFullscreen)), animated: transition.isAnimated) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatOptionsButton.swift b/submodules/TelegramCallsUI/Sources/VoiceChatOptionsButton.swift index f5cdd8f9c6..d494e4b5e2 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatOptionsButton.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatOptionsButton.swift @@ -6,6 +6,7 @@ import Postbox import AccountContext import TelegramPresentationData import AvatarNode +import AnimationUI func optionsBackgroundImage(dark: Bool) -> UIImage? { return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in @@ -30,6 +31,15 @@ func optionsButtonImage(dark: Bool) -> UIImage? { }) } +func optionsCircleImage(dark: Bool) -> UIImage? { + return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(UIColor(rgb: dark ? 0x1c1c1e : 0x2c2c2e).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + }) +} + func closeButtonImage(dark: Bool) -> UIImage? { return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -51,6 +61,7 @@ func closeButtonImage(dark: Bool) -> UIImage? { final class VoiceChatHeaderButton: HighlightableButtonNode { enum Content { case image(UIImage?) + case more(UIImage?) case avatar(Peer) } @@ -60,6 +71,7 @@ final class VoiceChatHeaderButton: HighlightableButtonNode { let referenceNode: ContextReferenceContentNode let containerNode: ContextControllerSourceNode private let iconNode: ASImageNode + private var animationNode: AnimationNode? private let avatarNode: AvatarNode var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? @@ -109,6 +121,15 @@ final class VoiceChatHeaderButton: HighlightableButtonNode { private var content: Content? func setContent(_ content: Content, animated: Bool = false) { + if case .more = content, self.animationNode == nil { + let iconColor = UIColor(rgb: 0xffffff) + let animationNode = AnimationNode(animation: "anim_profilemore", colors: ["Point 2.Group 1.Fill 1": iconColor, + "Point 3.Group 1.Fill 1": iconColor, + "Point 1.Group 1.Fill 1": iconColor], scale: 1.0) + animationNode.frame = self.containerNode.bounds + self.addSubnode(animationNode) + self.animationNode = animationNode + } if animated { switch content { case let .image(image): @@ -127,7 +148,12 @@ final class VoiceChatHeaderButton: HighlightableButtonNode { self.avatarNode.setPeer(context: self.context, theme: self.theme, peer: peer) self.iconNode.isHidden = true self.avatarNode.isHidden = false - + self.animationNode?.isHidden = true + case let .more(image): + self.iconNode.image = image + self.iconNode.isHidden = false + self.avatarNode.isHidden = true + self.animationNode?.isHidden = false } } else { self.content = content @@ -140,6 +166,12 @@ final class VoiceChatHeaderButton: HighlightableButtonNode { self.avatarNode.setPeer(context: self.context, theme: self.theme, peer: peer) self.iconNode.isHidden = true self.avatarNode.isHidden = false + self.animationNode?.isHidden = true + case let .more(image): + self.iconNode.image = image + self.iconNode.isHidden = false + self.avatarNode.isHidden = true + self.animationNode?.isHidden = false } } } @@ -155,4 +187,8 @@ final class VoiceChatHeaderButton: HighlightableButtonNode { func onLayout() { } + + func play() { + self.animationNode?.playOnce() + } } diff --git a/submodules/TelegramCore/Sources/StickerPackCreation.swift b/submodules/TelegramCore/Sources/StickerPackCreation.swift new file mode 100644 index 0000000000..fecc4ab449 --- /dev/null +++ b/submodules/TelegramCore/Sources/StickerPackCreation.swift @@ -0,0 +1 @@ +import Foundation diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift index 233b536b94..21d6342e2f 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift @@ -56,7 +56,7 @@ public struct PresentationResourcesSettings { public static let support = renderIcon(name: "Settings/MenuIcons/Support") public static let faq = renderIcon(name: "Settings/MenuIcons/Faq") - public static let tips = renderIcon(name: "Settings/MenuIcons/Faq") + public static let tips = renderIcon(name: "Settings/MenuIcons/Tips") public static let addAccount = renderIcon(name: "Settings/MenuIcons/AddAccount") public static let setPasscode = renderIcon(name: "Settings/MenuIcons/SetPasscode") diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 008d2d066a..0076564c95 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -458,8 +458,14 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = addAttributesToStringWithRanges(titleString, body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: attributePeerIds)) } } else if let duration = duration { - let titleString = strings.Notification_VoiceChatEnded(callDurationString(strings: strings, value: duration)).0 - attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor) + if message.author?.id.namespace == Namespaces.Peer.CloudChannel { + let titleString = strings.Notification_VoiceChatEnded(callDurationString(strings: strings, value: duration)).0 + attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor) + } else { + let attributePeerIds: [(Int, PeerId?)] = [(0, message.author?.id)] + let titleString = strings.Notification_VoiceChatEndedGroup(authorName, callDurationString(strings: strings, value: duration)) + attributedString = addAttributesToStringWithRanges(titleString, body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: attributePeerIds)) + } } else { if message.author?.id.namespace == Namespaces.Peer.CloudChannel { let titleString = strings.Notification_VoiceChatStartedChannel diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 49302fd909..9642bf8b48 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -220,6 +220,7 @@ swift_library( "//submodules/Speak:Speak", "//submodules/PeerInfoAvatarListNode:PeerInfoAvatarListNode", "//submodules/DebugSettingsUI:DebugSettingsUI", + "//submodules/ImportStickerPackUI:ImportStickerPackUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Contents.json index 38f0c81fc2..6e965652df 100644 --- a/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Contents.json @@ -1,9 +1,9 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 }, "properties" : { "provides-namespace" : true } -} \ No newline at end of file +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Tips.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Tips.imageset/Contents.json new file mode 100644 index 0000000000..d64de0bbcb --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Tips.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "tips_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Tips.imageset/tips_30.pdf b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Tips.imageset/tips_30.pdf new file mode 100644 index 0000000000..4c56ae4a56 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Tips.imageset/tips_30.pdf @@ -0,0 +1,113 @@ +%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 0.000000 0.000000 cm +0.964706 0.768627 0.262745 scn +0.000000 18.799999 m +0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c +1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c +5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c +18.799999 30.000000 l +22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c +27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c +30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c +30.000000 11.200001 l +30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c +28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c +24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c +11.200000 0.000000 l +7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c +2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c +0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c +0.000000 18.799999 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 8.500000 5.000000 cm +1.000000 1.000000 1.000000 scn +0.000000 12.500000 m +0.000000 16.089851 2.910149 19.000000 6.500000 19.000000 c +10.089851 19.000000 13.000000 16.089851 13.000000 12.500000 c +13.000000 9.326989 11.698906 7.884006 10.621044 6.688600 c +10.292052 6.323730 9.983858 5.981926 9.739806 5.621033 c +9.164136 4.769756 8.876301 4.344118 8.752400 4.243239 c +8.663695 4.171017 8.625642 4.138249 8.582385 4.115283 c +8.539128 4.092316 8.490668 4.079148 8.381149 4.046125 c +8.228177 4.000000 7.977118 4.000000 7.475001 4.000000 c +5.525000 4.000000 l +5.022882 4.000000 4.771823 4.000000 4.618851 4.046125 c +4.509332 4.079148 4.460872 4.092316 4.417615 4.115283 c +4.374358 4.138249 4.336305 4.171017 4.247600 4.243239 c +4.123699 4.344119 3.835864 4.769756 3.260195 5.621032 c +3.016143 5.981926 2.707948 6.323730 2.378956 6.688601 c +1.301094 7.884007 0.000000 9.326990 0.000000 12.500000 c +h +5.500000 3.000000 m +4.947715 3.000000 4.500000 2.552284 4.500000 2.000000 c +4.500000 0.895432 5.395431 0.000000 6.500000 0.000000 c +7.604569 0.000000 8.500000 0.895430 8.500000 2.000000 c +8.500000 2.552284 8.052285 3.000000 7.500000 3.000000 c +5.500000 3.000000 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 2149 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.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 + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002239 00000 n +0000002262 00000 n +0000002435 00000 n +0000002509 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2568 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Wallet.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Wallet.imageset/Contents.json deleted file mode 100644 index 490f9cdd8e..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Wallet.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "ic_wallet.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Wallet.imageset/ic_wallet.pdf b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Wallet.imageset/ic_wallet.pdf deleted file mode 100644 index 768f6dadc48c64de1ccc00cb315b071cad087c57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4511 zcmai22UHVV*QG=P0VzrcWk9M5BqTJI9y$_=A{_#R-VC8C(m|SZ=^X{6DkvSKgZKkRPJc%i z1OY$+7YlpHjT?Zl2FBS2XA6iDARRzh$<7gn!4l4nXdDKKv2?M*0MgPBHyjp&c7k{j zV{WNF;ufcBT)cu4gR+)$d1oCOh_f-CAn=#uCSQok1X+u62HNkGVxj zBRVRPQ?dlxxo1O;dUA_RXI2tv!!vbYQ)O)NeSy{^W6MCznT@tGDP1(^#D`u~%y_lI#G zLrE)bz~e!OERlqdaXblXuj#Q7)?6!O^VHcGOOA^XkGK<%AyS^0VLyF&4X9719{8MY zJ&Fuo{^M>%ywFDX9=rS?w$7Q$O;EXa5vLb~2XBGo?bTy@Mzd0yD<7FQTx)z7PTe!% zsq!4Yy5aFqWTJgAXv|-tj^FUiRI%M7Q3HibWchRIu7jj0NYlJA)OHdz|65_>3H>4N z0_N`(w7uogwM64W$oh;822{|64?D3uR%qmud`(bk*Yz5z8*vO@I{TM= z`3=@eja4+;hbN&CCH*Ej`57b3YP^wx;^rxuBi_YD7tKK7j#7fq7>Hm{`gmSFhE$g{ zhzK59m~tsP1*~cs>%Uj6Q-cf{6v|EK_d!mCY;n~UF3D7T3Ou^eq&}|MS}2hcU7NQL z0oUpCSzjK>i6-#|e|SFY)~B&;L)ybpv11`OY3e2yEKh8)K-JSIs3m_y32PD(5|%p^wlhK1fG`r{VP}caQI`MT zde409c}sVkF$~`%-qe4N3*gyi^`QXXUsquOX@{%&YmtNo^Lil9DACgi=vm!*O{^To zWvNI{85C$SO0h+&)zzwvRGgoTa-i5K6a6-ocHC_+J-%2oI&QP!J6cbqbrc9{!|4DN&z}zWVBMY|sE#uP{ALt%S_4pj#-sD}y zXURon8B9B_mI^m9676ssreCOPPP$E0?L;O!KU0d@0Eo8E5LHiQe7hLxXd2(~nTn4x za&^_wjX2vu=*k!I!|XtpH!Y!(yQzKahkDnaGRXv7Qe}W+6z887K2=A)eM?21jJG=8 zr9GK_Eot{hZ~N7Z?AJ`2AzyB5e@UbW4!3Oxxe=frD`W);_8Xq@V;18BvpmRjptp55 zBk~QA)T&hl_qa$ADO@7uc9!uW@m(>Zg+Y3n8*R(tD%@g3eEwWK;Gi^)aGueS^5k7*$)65(Y$Z7t3&!O@5CK9^lB?!t+5O?Jq*DM0?qM6%spH&e0=$ljG`D8Q#5q!}pYN@fhY^ZKkSC6&LjEF`p59ZEbEU`HPKm6jn-?pZ8j z0Zpp>2rZi-xwnFxmIOU%r$TrvZM^J$KW`E9<)CM|3R+UzmvF505**1~#vHIM^bes-xTm>Y>ifOUb1#ETEDsgw`NXf!-(J9P~T`aR<22nE>dgu`g zHRy1PWs05#(}I{aEj(a9@QKCn^QZcJr7|8AVsz=zf*pG1G3G{<$jZ zd}eojP|IvA;w)MYs-YQ;YVcoaKRd&_>D737ihd)Irq%7M#eL3Zh#$M(#pxK1t`@#n z3PX@XAPwhZfZLJ#7A-6JBKdJJT^m9>pa_NI3Z?b#Ft!kQ{%B0DQI(xnGl5<;l$%M1 zTOhSM;FhXAmkF;FZxd%i?=>rBEVF^EDXN@18!+RciI>qz5; zQ~UGjQ7wsft8A-oKz^lOuF_0(n^cIZVp?93#Y2lUi*Spe&!U~$JdN=I@ipgA#j;D4 z?|GhZvdRxW@qS-heY5)72h0cP1!eJq!@-25&TWn_P1_V09*ogE|2+FV>wMK49kMp4 z`5s({uuI# zM3*Q_nsZuj+I-p~qPyDGkn$zSaNcn7r71Kib^YxvYo4k|$=tH?wL-xG>4M1;sWQu= zDt()L&8m0jP2z=&#N}#?Yji#3QR>3_!e$lP8@bZj*=5-UEy^uMes5m)10T#I<Ey!8vSPt$kCrvx>uDyHlZ*|T4fq}E9j-&(Ll{^p z%qy&kdUp)x!QT+i>!D` z(}&3wcjE3;)Wq*vY}juUN?(%>myVV`-|W#`;w!v|J|;Q#IGFr4`gQSO^biG-0F_dF zffRt0Kzd+lN)d{4XWT*Ut;DSjfgHl`&A5$+7)j)h6g)z1P>52QFdVos3tdH&@~(Q< z-u8OnSxI8x)9v>angI@#>Ad@>x!Yx(HRT)%gNlyIr@`-L zVBL`N=ueIBDpB0&JV6GORrW8YY?LwCZ?xaiEo{%F%{lj7=>67v_(0TJ)YILj+2ha9 zqgK&%2kNqrk#ggYb()?gt@ShCn^$6-04J64uYF2=(HORA=ZL8x?HujOW^>!BjfF1x zI(hAxCc$?`)Q+8w432A))>UQpACtLE2fkbMeUA8y;+cqFnKtmfvp@NECwQ&t$l+C~0%G;`GxU<@!vPGc8i6;mxz^rA_v!d`i} zaJgVPfHfv_*YnxC-7d$UzAM~2U$UeH&#&79 zNpMPTe(v7m*vO)~G437V`)rBtID6++zhQ)7?#RSQ_^SdjyL%ekvt^H(&-gALm>ddE z?bQ3uKRU@|BaI|GSQl!(cVaprwd6Z>JOD1HF_n4iBXMkdY`Hp{#|J@O&Uz+Oc09Z{ zYF*s4y_z#>j<0%X6BAmiAbgUvUx8{rgD=KkQL0v2RxVS<6BQO*R(29bX*=R#Iiuriz*+4@P#J7OlR_4SL#bJS~ivDruJ42{_I@ zwA}8FTUtf!p^~zrWjyaw9Bp~=I$1AF?*@N6{I`ETb)i2@vlvX|m*GA2ucsKnljw7UiJT>w^I0@iQ7pvc||IXhF zi?N140T>(t{l5(m6BUJv0@lD!3_(Z+_%RDO|H7aWqJ*aBbo(2Fi;ELh&vEz)suAymz2We^rp)!EvGP(pu=k%S_;A!a3MjS)k`5D0Nmj3^pm sDJ}uSK%p2B1Pp3n3AL1l{O^#ziqH*5D2*Q<1Ob&qKzMi*QA&{i02xTW<^TWy diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index 505570d638..0ac557bebc 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -949,8 +949,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break case let .joinVoiceChat(peerId, invite): strongSelf.presentController(VoiceChatJoinScreen(context: strongSelf.context, peerId: peerId, invite: invite, join: { call in - }), .window(.root), nil) + case .importStickers: + break } } })) diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index ab9554b03b..d15ceb433b 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -23,6 +23,7 @@ import ShareController import ChatInterfaceState import TelegramCallsUI import UndoUI +import ImportStickerPackUI private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer { if case .default = navigation { @@ -480,5 +481,16 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur }), on: .root, blockInteraction: false, completion: {}) })) } + case .importStickers: + dismissInput() + if let navigationController = navigationController, let data = UIPasteboard.general.data(forPasteboardType: "org.telegram.third-party.stickerpack"), let stickerPack = ImportStickerPack(data: data) { + for controller in navigationController.overlayControllers { + if controller is ImportStickerPackController { + controller.dismiss() + } + } + let controller = ImportStickerPackController(context: context, stickerPack: stickerPack, parentNavigationController: navigationController) + present(controller, nil) + } } } diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 218059f25c..c1cdd4d3be 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -678,7 +678,9 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } } } else { - if parsedUrl.host == "settings" { + if parsedUrl.host == "importStickers" { + handleResolvedUrl(.importStickers) + } else if parsedUrl.host == "settings" { if let path = parsedUrl.pathComponents.last { var section: ResolvedUrlSettingsSection? switch path {