diff --git a/submodules/Components/PagerComponent/Sources/PagerComponent.swift b/submodules/Components/PagerComponent/Sources/PagerComponent.swift index 018c053c55..6897e9c4a2 100644 --- a/submodules/Components/PagerComponent/Sources/PagerComponent.swift +++ b/submodules/Components/PagerComponent/Sources/PagerComponent.swift @@ -186,6 +186,7 @@ public final class PagerComponent Void)? public let isTopPanelExpandedUpdated: (Bool, Transition) -> Void public let isTopPanelHiddenUpdated: (Bool, Transition) -> Void + public let contentIdUpdated: (AnyHashable) -> Void public let panelHideBehavior: PagerComponentPanelHideBehavior public let clipContentToTopPanel: Bool @@ -205,6 +206,7 @@ public final class PagerComponent Void)?, isTopPanelExpandedUpdated: @escaping (Bool, Transition) -> Void, isTopPanelHiddenUpdated: @escaping (Bool, Transition) -> Void, + contentIdUpdated: @escaping (AnyHashable) -> Void, panelHideBehavior: PagerComponentPanelHideBehavior, clipContentToTopPanel: Bool ) { @@ -223,6 +225,7 @@ public final class PagerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let previousPanelHideBehavior = self.component?.panelHideBehavior @@ -418,21 +442,7 @@ public final class PagerComponent take(1) - |> deliverOnMainQueue).start(next: { [weak self] status in + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] status in self?.started?(status.duration) }) } @@ -273,7 +273,7 @@ final class DrawingStickerEntityView: DrawingEntityView { self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384) - |> deliverOn(Queue.concurrentDefaultQueue())).start()) + |> deliverOn(Queue.concurrentDefaultQueue())).start()) } } self.animationNode?.visibility = isPlaying diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/DrawingUI/Sources/StickerPickerScreen.swift index 74cee9670c..fc76d525f9 100644 --- a/submodules/DrawingUI/Sources/StickerPickerScreen.swift +++ b/submodules/DrawingUI/Sources/StickerPickerScreen.swift @@ -154,6 +154,7 @@ private final class StickerSelectionComponent: Component { switchToGifSubject: { _ in }, reorderItems: { _, _ in }, makeSearchContainerNode: { _ in return nil }, + contentIdUpdated: { _ in }, deviceMetrics: component.deviceMetrics, hiddenInputHeight: 0.0, inputHeight: 0.0, diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAvatarMenuMixin.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAvatarMenuMixin.h index 6c69dadda9..4224dbd29e 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAvatarMenuMixin.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAvatarMenuMixin.h @@ -26,6 +26,8 @@ typedef void (^TGMediaAvatarPresentImpl)(id, void (^)(U @property (nonatomic, copy) void (^requestSearchController)(TGMediaAssetsController *); @property (nonatomic, copy) CGRect (^sourceRect)(void); +@property (nonatomic, copy) void (^requestAvatarEditor)(void (^)(UIImage *image, NSURL *asset, TGVideoEditAdjustments *adjustments, void(^commit)(void))); + @property (nonatomic, strong) id stickersContext; - (instancetype)initWithContext:(id)context parentController:(TGViewController *)parentController hasDeleteButton:(bool)hasDeleteButton saveEditedPhotos:(bool)saveEditedPhotos saveCapturedMedia:(bool)saveCapturedMedia; diff --git a/submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m b/submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m index 2399255d01..b6c3e15132 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m +++ b/submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m @@ -193,24 +193,60 @@ }]; [itemViews addObject:galleryItem]; -// if (_hasSearchButton) -// { -// TGMenuSheetButtonItemView *viewItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"ProfilePhoto.SearchWeb") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ -// { -// __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf; -// if (strongSelf == nil) -// return; -// -// __strong TGMenuSheetController *strongController = weakController; -// if (strongController == nil) -// return; -// -// [strongController dismissAnimated:true]; -// if (strongSelf != nil) -// strongSelf.requestSearchController(nil); -// }]; -// [itemViews addObject:viewItem]; -// } + if (!_signup) { + TGMenuSheetButtonItemView *viewItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:@"Emoji or Sticker" type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ + { + __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + __strong TGMenuSheetController *strongController = weakController; + if (strongController == nil) + return; + + [strongController dismissAnimated:true]; + if (strongSelf != nil) + strongSelf.requestAvatarEditor(^(UIImage *image, NSURL *asset, TGVideoEditAdjustments *adjustments, void (^commit)(void)) { + __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf.willFinishWithVideo != nil) { + strongSelf.willFinishWithVideo(image, ^{ + if (strongSelf.didFinishWithVideo != nil) + strongSelf.didFinishWithVideo(image, asset, adjustments); + + commit(); + }); + } else { + if (strongSelf.didFinishWithVideo != nil) + strongSelf.didFinishWithVideo(image, asset, adjustments); + + commit(); + } + }); + }]; + [itemViews addObject:viewItem]; + } + + if (_hasSearchButton) + { + TGMenuSheetButtonItemView *viewItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"ProfilePhoto.SearchWeb") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ + { + __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + __strong TGMenuSheetController *strongController = weakController; + if (strongController == nil) + return; + + [strongController dismissAnimated:true]; + if (strongSelf != nil) + strongSelf.requestSearchController(nil); + }]; + [itemViews addObject:viewItem]; + } if (_hasViewButton) { diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 2d2a8a7280..6d3ff83128 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -351,6 +351,7 @@ swift_library( "//submodules/TelegramUI/Components/ChatInputNode", "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", "//submodules/TelegramUI/Components/StorageUsageScreen", + "//submodules/TelegramUI/Components/AvatarEditorScreen", "//submodules/MediaPasteboardUI:MediaPasteboardUI", "//submodules/DrawingUI:DrawingUI", "//submodules/FeaturedStickersScreen:FeaturedStickersScreen", diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/BUILD b/submodules/TelegramUI/Components/AvatarEditorScreen/BUILD new file mode 100644 index 0000000000..3b9f0db4cc --- /dev/null +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/BUILD @@ -0,0 +1,43 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AvatarEditorScreen", + module_name = "AvatarEditorScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/Components/ViewControllerComponent:ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent:MultilineTextComponent", + "//submodules/Components/SolidRoundedButtonComponent", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/AccountContext:AccountContext", + "//submodules/AppBundle:AppBundle", + "//submodules/TelegramStringFormatting:TelegramStringFormatting", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/ContextUI", + "//submodules/UndoUI", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", + "//submodules/TelegramNotices:TelegramNotices", + "//submodules/Markdown:Markdown", + "//submodules/GradientBackground:GradientBackground", + "//submodules/LegacyComponents:LegacyComponents", + "//submodules/DrawingUI:DrawingUI", + "//submodules/StickerResources:StickerResources", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift new file mode 100644 index 0000000000..028ae40b62 --- /dev/null +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift @@ -0,0 +1,1194 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import MultilineTextComponent +import EmojiStatusComponent +import Postbox +import TelegramStringFormatting +import TelegramNotices +import EntityKeyboard +import PagerComponent +import Markdown +import GradientBackground +import LegacyComponents +import DrawingUI +import SolidRoundedButtonComponent + +enum AvatarBackground: Equatable { + case gradient([UInt32]) + + var colors: [UInt32] { + switch self { + case let .gradient(colors): + return colors + } + } + + func generateImage(size: CGSize) -> UIImage { + switch self { + case let .gradient(colors): + if colors.count == 1 { + return generateSingleColorImage(size: size, color: UIColor(rgb: colors.first!))! + } else if colors.count == 2 { + return generateGradientImage(size: size, colors: colors.map { UIColor(rgb: $0) }, locations: [0.0, 1.0])! + } else { + return GradientBackgroundNode.generatePreview(size: size, colors: colors.map { UIColor(rgb: $0) }) + } + } + } +} + +private let defaultBackgrounds: [AvatarBackground] = [ + .gradient([0x72d5fd, 0x2a9ef1]), + .gradient([0xff885e, 0xff516a]), + .gradient([0xffcd6a, 0xffa85c]), + .gradient([0xa0de7e, 0x54cb68]), + .gradient([0x00fcfd, 0x4acccd]), + .gradient([0xe0a2f3, 0xd669ed]), + .gradient([0x82b1ff, 0x665fff]), +] + +private struct KeyboardInputData: Equatable { + var emoji: EmojiPagerContentComponent + var stickers: EmojiPagerContentComponent? + + init( + emoji: EmojiPagerContentComponent, + stickers: EmojiPagerContentComponent? + ) { + self.emoji = emoji + self.stickers = stickers + } +} + +final class AvatarEditorScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let ready: Promise + + init( + context: AccountContext, + ready: Promise + ) { + self.context = context + self.ready = ready + } + + static func ==(lhs: AvatarEditorScreenComponent, rhs: AvatarEditorScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class State: ComponentState { + let context: AccountContext + + var selectedBackground: AvatarBackground + var selectedItem: EmojiPagerContentComponent.Item? + + var keyboardContentId: AnyHashable = "emoji" + var expanded: Bool = false + var editingColor: Bool = false + var previousColor: AvatarBackground + + var isSearchActive: Bool = false + + init(context: AccountContext) { + self.context = context + + self.selectedBackground = defaultBackgrounds.first! + self.previousColor = self.selectedBackground + } + } + + func makeState() -> State { + return State( + context: self.context + ) + } + + class View: UIView, UIScrollViewDelegate { + private let navigationCancelButton = ComponentView() + private let navigationDoneButton = ComponentView() + + private let previewContainerView: UIView + private let previewView = ComponentView() + + private let backgroundContainerView: UIView + private let backgroundTitleView = ComponentView() + private let backgroundView = ComponentView() + private let colorPickerView = ComponentView() + + private let keyboardContainerView: UIView + private let keyboardTitleView = ComponentView() + private let keyboardSwitchView = ComponentView() + private let keyboardView = ComponentView() + private let panelBackgroundView: BlurredBackgroundView + private let panelHostView: PagerExternalTopPanelContainer + private let panelSeparatorView: UIView + + private let buttonView = ComponentView() + + private var component: AvatarEditorScreenComponent? + private weak var state: State? + + private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)? + private var controller: (() -> AvatarEditorScreen?)? + + private var dataDisposable: Disposable? + private var data: KeyboardInputData? + + private let emojiSearchDisposable = MetaDisposable() + private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?>(nil) + + private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? + + override init(frame: CGRect) { + self.previewContainerView = UIView() + self.previewContainerView.clipsToBounds = true + if #available(iOS 13.0, *) { + self.previewContainerView.layer.cornerCurve = .circular + } + + self.backgroundContainerView = UIView() + self.backgroundContainerView.clipsToBounds = true + self.backgroundContainerView.layer.cornerRadius = 10.0 + + self.keyboardContainerView = UIView() + self.keyboardContainerView.clipsToBounds = true + self.keyboardContainerView.layer.cornerRadius = 10.0 + + self.panelBackgroundView = BlurredBackgroundView(color: .white) + self.panelHostView = PagerExternalTopPanelContainer() + self.panelSeparatorView = UIView() + + super.init(frame: frame) + + self.addSubview(self.previewContainerView) + self.addSubview(self.backgroundContainerView) + self.addSubview(self.keyboardContainerView) + self.keyboardContainerView.addSubview(self.panelBackgroundView) + self.keyboardContainerView.addSubview(self.panelHostView) + self.keyboardContainerView.addSubview(self.panelSeparatorView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.dataDisposable?.dispose() + self.emojiSearchDisposable.dispose() + } + + private func updateData(_ data: KeyboardInputData) { + self.data = data + + self.state?.selectedItem = data.emoji.itemGroups.first?.items.first + self.state?.updated(transition: .immediate) + + let updateSearchQuery: (String, String) -> Void = { [weak self] rawQuery, languageCode in + guard let strongSelf = self, let context = strongSelf.state?.context else { + return + } + + let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) + + if query.isEmpty { + strongSelf.emojiSearchDisposable.set(nil) + strongSelf.emojiSearchResult.set(.single(nil)) + } else { + var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) + if !languageCode.lowercased().hasPrefix("en") { + signal = signal + |> mapToSignal { keywords in + return .single(keywords) + |> then( + context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3) + |> map { englishKeywords in + return keywords + englishKeywords + } + ) + } + } + + let resultSignal = signal + |> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + return combineLatest( + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1), + combineLatest(keywords.map { context.engine.stickers.searchStickers(query: $0.emoticons.first!) }) + ) + |> map { view, stickers -> [EmojiPagerContentComponent.ItemGroup] in + let hasPremium = true + + var emojis: [(String, TelegramMediaFile?, String)] = [] + + var existingEmoticons = Set() + var allEmoticons: [String: String] = [:] + for keyword in keywords { + for emoticon in keyword.emoticons { + allEmoticons[emoticon] = keyword.keyword + + if !existingEmoticons.contains(emoticon) { + existingEmoticons.insert(emoticon) + } + } + } + + for entry in view.entries { + guard let item = entry.item as? StickerPackItem else { + continue + } + for attribute in item.file.attributes { + switch attribute { + case let .CustomEmoji(_, _, alt, _): + if !item.file.isPremiumEmoji || hasPremium { + if !alt.isEmpty, let keyword = allEmoticons[alt] { + emojis.append((alt, item.file, keyword)) + } else if alt == query { + emojis.append((alt, item.file, alt)) + } + } + default: + break + } + } + } + + var emojiItems: [EmojiPagerContentComponent.Item] = [] + var existingIds = Set() + for item in emojis { + if let itemFile = item.1 { + if existingIds.contains(itemFile.fileId) { + continue + } + existingIds.insert(itemFile.fileId) + let animationData = EntityKeyboardAnimationData(file: itemFile) + let item = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: itemFile, + subgroupId: nil, + icon: .none, + tintMode: animationData.isTemplate ? .primary : .none + ) + emojiItems.append(item) + } + } + + var stickerItems: [EmojiPagerContentComponent.Item] = [] + for stickerResult in stickers { + for sticker in stickerResult { + if existingIds.contains(sticker.file.fileId) { + continue + } + + existingIds.insert(sticker.file.fileId) + let animationData = EntityKeyboardAnimationData(file: sticker.file) + let item = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: sticker.file, + subgroupId: nil, + icon: .none, + tintMode: .none + ) + stickerItems.append(item) + } + } + + var result: [EmojiPagerContentComponent.ItemGroup] = [] + if !emojiItems.isEmpty { + result.append( + EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "emoji", + title: "Emoji", + subtitle: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + items: emojiItems + ) + ) + } + if !stickerItems.isEmpty { + result.append( + EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "stickers", + title: "Stickers", + subtitle: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + items: stickerItems + ) + ) + } + return result + } + } + + strongSelf.emojiSearchDisposable.set((resultSignal + |> delay(0.15, queue: .mainQueue()) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query)))) + })) + } + } + + data.emoji.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( + performItemAction: { [weak self] _, item, _, _, _, _ in + guard let self, let _ = item.itemFile else { + return + } + self.state?.selectedItem = item + self.state?.updated(transition: .easeInOut(duration: 0.2)) + }, + deleteBackwards: nil, + openStickerSettings: nil, + openFeatured: nil, + openSearch: { + }, + addGroupAction: { [weak self] groupId, isPremiumLocked in + guard let strongSelf = self, let controller = strongSelf.controller?(), let collectionId = groupId.base as? ItemCollectionId else { + return + } + let context = controller.context + let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks) + let _ = (context.account.postbox.combinedView(keys: [viewKey]) + |> take(1) + |> deliverOnMainQueue).start(next: { views in + guard let view = views.views[viewKey] as? OrderedItemListView else { + return + } + for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { + if featuredStickerPack.info.id == collectionId { + let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), forceActualized: false) + |> mapToSignal { result -> Signal in + switch result { + case let .result(info, items, installed): + if installed { + return .complete() + } else { + return context.engine.stickers.addStickerPackInteractively(info: info, items: items) + } + case .fetching: + break + case .none: + break + } + return .complete() + } + |> deliverOnMainQueue).start(completed: { + }) + + break + } + } + }) + }, + clearGroup: { [weak self] groupId in + guard let strongSelf = self, let controller = strongSelf.controller?() else { + return + } + if groupId == AnyHashable("popular") { + let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) + let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize)) + var items: [ActionSheetItem] = [] + let context = controller.context + items.append(ActionSheetTextItem(title: presentationData.strings.Chat_ClearReactionsAlertText, parseMarkdown: true)) + items.append(ActionSheetButtonItem(title: presentationData.strings.Chat_ClearReactionsAlertAction, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + guard let strongSelf = self else { + return + } + + strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupRemoved(id: "popular")) + let _ = context.engine.stickers.clearRecentlyUsedReactions().start() + })) + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet) + } + }, + pushController: { c in + }, + presentController: { c in + }, + presentGlobalOverlayController: { c in + }, + navigationController: { [weak self] in + return self?.controller?()?.navigationController as? NavigationController + }, + requestUpdate: { [weak self] transition in + guard let strongSelf = self else { + return + } + if !transition.animation.isImmediate { + strongSelf.state?.updated(transition: transition) + } + }, + updateSearchQuery: { rawQuery, languageCode in + updateSearchQuery(rawQuery, languageCode) + }, + updateScrollingToItemGroup: { + }, + chatPeerId: nil, + peekBehavior: nil, + customLayout: nil, + externalBackground: nil, + externalExpansionView: nil, + useOpaqueTheme: false, + hideBackground: true + ) + +// var stickerPeekBehavior: EmojiContentPeekBehaviorImpl? +// if let controller = self.controller { +// stickerPeekBehavior = EmojiContentPeekBehaviorImpl( +// context: controller.context, +// interaction: nil, +// chatPeerId: nil, +// present: { [weak controller] c, a in +// controller?.presentInGlobalOverlay(c, with: a) +// } +// ) +// } + + data.stickers?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( + performItemAction: { [weak self] _, item, _, _, _, _ in + guard let self, let _ = item.itemFile else { + return + } + self.state?.selectedItem = item + self.state?.updated(transition: .easeInOut(duration: 0.2)) + }, + deleteBackwards: nil, + openStickerSettings: nil, + openFeatured: nil, + openSearch: { + }, + addGroupAction: { [weak self] groupId, isPremiumLocked in + guard let strongSelf = self, let controller = strongSelf.controller?(), let collectionId = groupId.base as? ItemCollectionId else { + return + } + let context = controller.context + let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks) + let _ = (context.account.postbox.combinedView(keys: [viewKey]) + |> take(1) + |> deliverOnMainQueue).start(next: { views in + guard let view = views.views[viewKey] as? OrderedItemListView else { + return + } + for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { + if featuredStickerPack.info.id == collectionId { + let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), forceActualized: false) + |> mapToSignal { result -> Signal in + switch result { + case let .result(info, items, installed): + if installed { + return .complete() + } else { + return context.engine.stickers.addStickerPackInteractively(info: info, items: items) + } + case .fetching: + break + case .none: + break + } + return .complete() + } + |> deliverOnMainQueue).start(completed: { + }) + + break + } + } + }) + }, + clearGroup: { [weak self] groupId in + guard let strongSelf = self, let controller = strongSelf.controller?() else { + return + } + let context = controller.context + if groupId == AnyHashable("recent") { + let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) + let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize)) + var items: [ActionSheetItem] = [] + items.append(ActionSheetButtonItem(title: presentationData.strings.Stickers_ClearRecent, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + let _ = context.engine.stickers.clearRecentlyUsedStickers().start() + })) + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet) + } else if groupId == AnyHashable("featuredTop") { + let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks) + let _ = (context.account.postbox.combinedView(keys: [viewKey]) + |> take(1) + |> deliverOnMainQueue).start(next: { views in + guard let view = views.views[viewKey] as? OrderedItemListView else { + return + } + var stickerPackIds: [Int64] = [] + for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { + stickerPackIds.append(featuredStickerPack.info.id.id) + } + let _ = ApplicationSpecificNotice.setDismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager, values: stickerPackIds).start() + }) + } else if groupId == AnyHashable("peerSpecific") { + } + }, + pushController: { c in + }, + presentController: { c in + }, + presentGlobalOverlayController: { c in + }, + navigationController: { [weak self] in + return self?.controller?()?.navigationController as? NavigationController + }, + requestUpdate: { [weak self] transition in + guard let strongSelf = self else { + return + } + if !transition.animation.isImmediate { + strongSelf.state?.updated(transition: transition) + } + }, + updateSearchQuery: { rawQuery, languageCode in + updateSearchQuery(rawQuery, languageCode) + }, + updateScrollingToItemGroup: { + }, + chatPeerId: nil, + peekBehavior: nil, + customLayout: nil, + externalBackground: nil, + externalExpansionView: nil, + useOpaqueTheme: false, + hideBackground: true + ) + } + + private var isExpanded = false + + func update(component: AvatarEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + + let controller = environment.controller + self.controller = { + return controller() as? AvatarEditorScreen + } + self.navigationMetrics = (environment.navigationHeight, environment.statusBarHeight) + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + let effectiveIsExpanded = state.expanded || state.editingColor + + if self.isExpanded != effectiveIsExpanded { + self.isExpanded = effectiveIsExpanded + + if let snapshotView = self.navigationCancelButton.view?.snapshotContentTree() { + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + self.addSubview(snapshotView) + } + } + + let navigationCancelButtonSize = self.navigationCancelButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: state.expanded ? .white : environment.theme.rootController.navigationBar.accentTextColor)), + action: { [weak self] in + guard let self else { + return + } + self.controller?()?.dismiss() + } + ).minSize(CGSize(width: 16.0, height: environment.navigationHeight - environment.statusBarHeight))), + environment: {}, + containerSize: CGSize(width: 150.0, height: environment.navigationHeight - environment.statusBarHeight) + ) + if let navigationCancelButtonView = self.navigationCancelButton.view { + if navigationCancelButtonView.superview == nil { + self.addSubview(navigationCancelButtonView) + } + transition.setFrame(view: navigationCancelButtonView, frame: CGRect(origin: CGPoint(x: 16.0 + environment.safeInsets.left, y: environment.statusBarHeight), size: navigationCancelButtonSize)) + transition.setAlpha(view: navigationCancelButtonView, alpha: !state.editingColor ? 1.0 : 0.0) + } + + let navigationDoneButtonSize = self.navigationDoneButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Text(text: "Set", font: Font.semibold(17.0), color: state.isSearchActive ? environment.theme.rootController.navigationBar.accentTextColor : .white)), + action: { [weak self] in + guard let self else { + return + } + self.complete() + } + ).minSize(CGSize(width: 16.0, height: environment.navigationHeight - environment.statusBarHeight))), + environment: {}, + containerSize: CGSize(width: 150.0, height: environment.navigationHeight - environment.statusBarHeight) + ) + if let navigationDoneButtonView = self.navigationDoneButton.view { + if navigationDoneButtonView.superview == nil { + self.addSubview(navigationDoneButtonView) + } + transition.setFrame(view: navigationDoneButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - navigationDoneButtonSize.width, y: environment.statusBarHeight), size: navigationDoneButtonSize)) + transition.setAlpha(view: navigationDoneButtonView, alpha: (state.expanded || state.isSearchActive) && !state.editingColor ? 1.0 : 0.0) + } + + self.backgroundColor = environment.theme.list.blocksBackgroundColor + self.backgroundContainerView.backgroundColor = environment.theme.list.plainBackgroundColor + self.keyboardContainerView.backgroundColor = environment.theme.list.plainBackgroundColor + self.panelSeparatorView.backgroundColor = environment.theme.list.itemPlainSeparatorColor + + if self.dataDisposable == nil { + let context = component.context + let emojiItems = EmojiPagerContentComponent.emojiInputData( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + isStandalone: false, + isStatusSelection: false, + isReactionSelection: false, + isEmojiSelection: false, + isProfilePhotoEmojiSelection: true, + topReactionItems: [], + areUnicodeEmojiEnabled: false, + areCustomEmojiEnabled: true, + chatPeerId: context.account.peerId, + hasSearch: true, + forceHasPremium: true + ) + + let stickerItems = EmojiPagerContentComponent.stickerInputData( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks], + stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers], + chatPeerId: context.account.peerId, + hasSearch: true, + hasTrending: false, + forceHasPremium: true, + searchIsPlaceholderOnly: false + ) + + let signal = combineLatest(queue: .mainQueue(), + emojiItems, + stickerItems, + self.emojiSearchResult.get() + ) |> map { emoji, stickers, searchResult -> (KeyboardInputData, (groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?) in + return (KeyboardInputData(emoji: emoji, stickers: stickers), searchResult) + } + self.dataDisposable = (signal + |> deliverOnMainQueue + ).start(next: { [weak self, weak state] data, searchResult in + if let self { + var data = data + + if let searchResult = searchResult { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? + if !searchResult.groups.contains(where: { !$0.items.isEmpty }) { + emptySearchResults = EmojiPagerContentComponent.EmptySearchResults( + text: presentationData.strings.EmojiSearch_SearchEmojiEmptyResult, + iconFile: nil + ) + } + + if state?.keyboardContentId == AnyHashable("emoji") { + data.emoji = data.emoji.withUpdatedItemGroups(itemGroups: searchResult.groups, itemContentUniqueId: searchResult.id, emptySearchResults: emptySearchResults) + } else { + data.stickers = data.stickers?.withUpdatedItemGroups(itemGroups: searchResult.groups, itemContentUniqueId: searchResult.id, emptySearchResults: emptySearchResults) + } + } + + self.updateData(data) + state?.updated(transition: .immediate) + } + }) + } + + var contentHeight: CGFloat = 0.0 + + let collapsedAvatarSize = CGSize(width: 100.0, height: 100.0) + let avatarPreviewSize = self.previewView.update( + transition: transition, + component: AnyComponent( + AvatarPreviewComponent( + context: component.context, + background: state.selectedBackground, + file: state.selectedItem?.itemFile, + tapped: { [weak state] in + if let state, !state.editingColor { + state.expanded = !state.expanded + state.updated(transition: .easeInOut(duration: 0.3)) + } + } + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: availableSize.width) + ) + if let previewView = self.previewView.view { + if previewView.superview == nil { + self.previewContainerView.addSubview(previewView) + } + + let previewScale = effectiveIsExpanded ? 1.0 : collapsedAvatarSize.width / avatarPreviewSize.width + let cornerRadius = effectiveIsExpanded ? 0.0 : availableSize.width / 2.0 + let position = effectiveIsExpanded ? avatarPreviewSize.height / 2.0 : environment.navigationHeight + 10.0 + + transition.setBounds(view: previewView, bounds: CGRect(origin: .zero, size: avatarPreviewSize)) + transition.setPosition(view: previewView, position: CGPoint(x: avatarPreviewSize.width / 2.0, y: avatarPreviewSize.height / 2.0)) + + transition.setBounds(view: self.previewContainerView, bounds: CGRect(origin: .zero, size: avatarPreviewSize)) + transition.setPosition(view: self.previewContainerView, position: CGPoint(x: availableSize.width / 2.0, y: position)) + transition.setTransform(view: self.previewContainerView, transform: CATransform3DMakeScale(previewScale, previewScale, 1.0)) + transition.setCornerRadius(layer: self.previewContainerView.layer, cornerRadius: cornerRadius) + + contentHeight += effectiveIsExpanded ? avatarPreviewSize.height : environment.navigationHeight + collapsedAvatarSize.height - 41.0 + } + contentHeight += 17.0 + + let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor) + let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor) + let link = MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor) + + let backgroundTitleSize = self.backgroundTitleView.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .markdown( + text: "Background".uppercased(), attributes: MarkdownAttributes( + body: body, + bold: bold, + link: body, + linkAttribute: { _ in nil } + ) + ), + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: .greatestFiniteMagnitude) + ) + let backgroundTitleFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: backgroundTitleSize) + if let backgroundTitleView = self.backgroundTitleView.view { + if backgroundTitleView.superview == nil { + self.addSubview(backgroundTitleView) + } + transition.setFrame(view: backgroundTitleView, frame: backgroundTitleFrame) + } + contentHeight += backgroundTitleSize.height + contentHeight += 8.0 + + let backgroundSize = self.backgroundView.update( + transition: transition, + component: AnyComponent(BackgroundColorComponent( + theme: environment.theme, + values: defaultBackgrounds, + selectedValue: state.selectedBackground, + updateValue: { [weak state] value in + if let state { + state.selectedBackground = value + state.updated(transition: .easeInOut(duration: 0.2)) + } + }, + openColorPicker: { [weak state] in + if let state { + state.editingColor = true + state.previousColor = state.selectedBackground + state.updated(transition: .easeInOut(duration: 0.3)) + } + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude) + ) + let backgroundFrame = CGRect(origin: .zero, size: backgroundSize) + if let backgroundView = self.backgroundView.view { + if backgroundView.superview == nil { + self.backgroundContainerView.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + transition.setAlpha(view: backgroundView, alpha: state.editingColor ? 0.0 : 1.0) + } + + let colorPickerSize = self.colorPickerView.update( + transition: transition, + component: AnyComponent( + ColorPickerComponent( + theme: environment.theme, + strings: environment.strings, + colors: state.selectedBackground.colors, + colorsChanged: { [weak state] colors in + if let state { + state.selectedBackground = .gradient(colors) + state.updated(transition: .immediate) + } + }, + cancel: { [weak state] in + if let state { + state.selectedBackground = state.previousColor + state.editingColor = false + state.updated(transition: .easeInOut(duration: 0.3)) + } + }, + done: { [weak state] in + if let state { + state.editingColor = false + state.updated(transition: .easeInOut(duration: 0.3)) + } + } + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude) + ) + let colorPickerFrame = CGRect(origin: .zero, size: colorPickerSize) + if let colorPickerView = self.colorPickerView.view { + if colorPickerView.superview == nil { + self.backgroundContainerView.addSubview(colorPickerView) + } + transition.setFrame(view: colorPickerView, frame: colorPickerFrame) + transition.setAlpha(view: colorPickerView, alpha: state.editingColor ? 1.0 : 0.0) + } + + let backgroundContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: state.editingColor ? colorPickerSize : backgroundSize) + transition.setFrame(view: self.backgroundContainerView, frame: backgroundContainerFrame) + + contentHeight += backgroundContainerFrame.height + contentHeight += 24.0 + + let keyboardTitle: String + let keyboardSwitchTitle: String + + if state.isSearchActive { + keyboardTitle = "Emoji or Sticker" + keyboardSwitchTitle = "" + } else if state.keyboardContentId == AnyHashable("emoji") { + keyboardTitle = "Emoji" + keyboardSwitchTitle = "Switch to Stickers" + } else if state.keyboardContentId == AnyHashable("stickers") { + keyboardTitle = "Stickers" + keyboardSwitchTitle = "Switch to Emoji" + } else { + keyboardTitle = "" + keyboardSwitchTitle = "" + } + + let keyboardTitleSize = self.keyboardTitleView.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .markdown( + text: keyboardTitle.uppercased(), attributes: MarkdownAttributes( + body: body, + bold: bold, + link: body, + linkAttribute: { _ in nil } + ) + ), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: .greatestFiniteMagnitude) + ) + let keyboardTitleFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keyboardTitleSize) + if let keyboardTitleView = self.keyboardTitleView.view { + if keyboardTitleView.superview == nil { + self.addSubview(keyboardTitleView) + } + transition.setFrame(view: keyboardTitleView, frame: keyboardTitleFrame) + transition.setAlpha(view: keyboardTitleView, alpha: state.editingColor ? 0.0 : 1.0) + } + + let keyboardSwitchSize = self.keyboardSwitchView.update( + transition: transition, + component: AnyComponent( + Button( + content: AnyComponent( + MultilineTextComponent( + text: .markdown( + text: keyboardSwitchTitle.uppercased(), attributes: MarkdownAttributes( + body: link, + bold: link, + link: link, + linkAttribute: { _ in nil } + ) + ), + maximumNumberOfLines: 1 + ) + ), action: { [weak self] in + if let strongSelf = self, let state = strongSelf.state { + if let strongSelf = self, let pagerView = strongSelf.keyboardView.view as? EntityKeyboardComponent.View { + let targetContentId: AnyHashable + if state.keyboardContentId == AnyHashable("emoji") { + targetContentId = AnyHashable("stickers") + } else { + targetContentId = AnyHashable("emoji") + } + pagerView.scrollToContentId(targetContentId) + } + } + } + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: .greatestFiniteMagnitude) + ) + let keyboardSwitchFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - 15.0 - keyboardSwitchSize.width, y: contentHeight), size: keyboardSwitchSize) + if let keyboardSwitchView = self.keyboardSwitchView.view { + if keyboardSwitchView.superview == nil { + self.addSubview(keyboardSwitchView) + } + transition.setFrame(view: keyboardSwitchView, frame: keyboardSwitchFrame) + transition.setAlpha(view: keyboardSwitchView, alpha: state.editingColor ? 0.0 : 1.0) + } + contentHeight += keyboardTitleSize.height + contentHeight += 8.0 + + var bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom : 16.0 + if !effectiveIsExpanded { + bottomInset += 50.0 + 16.0 + } + + let keyboardContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height - contentHeight - bottomInset)) + transition.setFrame(view: self.keyboardContainerView, frame: keyboardContainerFrame) + transition.setAlpha(view: self.keyboardContainerView, alpha: state.editingColor ? 0.0 : 1.0) + + let isSearchActive = state.isSearchActive + let topPanelHeight: CGFloat = isSearchActive ? 0.0 : 42.0 + + if let data = self.data { + let keyboardSize = self.keyboardView.update( + transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)), + component: AnyComponent(EntityKeyboardComponent( + theme: environment.theme, + strings: environment.strings, + isContentInFocus: false, + containerInsets: UIEdgeInsets(), + topPanelInsets: UIEdgeInsets(top: 0.0, left: topPanelHeight - 34.0, bottom: 0.0, right: 4.0), + emojiContent: data.emoji, + stickerContent: data.stickers, + maskContent: nil, + gifContent: nil, + hasRecentGifs: false, + availableGifSearchEmojies: [], + defaultToEmojiTab: true, + externalTopPanelContainer: self.panelHostView, + externalBottomPanelContainer: nil, + displayTopPanelBackground: true, + topPanelExtensionUpdated: { _, _ in }, + hideInputUpdated: { _, _, _ in }, + hideTopPanelUpdated: { [weak self] hideTopPanel, transition in + if let strongSelf = self { + strongSelf.state?.isSearchActive = hideTopPanel + strongSelf.state?.updated(transition: transition) + } + }, + switchToTextInput: {}, + switchToGifSubject: { _ in }, + reorderItems: { _, _ in }, + makeSearchContainerNode: { _ in return nil }, + contentIdUpdated: { [weak self] contentId in + if let strongSelf = self { + strongSelf.state?.keyboardContentId = contentId + strongSelf.state?.updated(transition: .immediate) + } + }, + deviceMetrics: environment.deviceMetrics, + hiddenInputHeight: 0.0, + inputHeight: 0.0, + displayBottomPanel: false, + isExpanded: true, + clipContentToTopPanel: false + )), + environment: {}, + containerSize: CGSize(width: keyboardContainerFrame.size.width, height: keyboardContainerFrame.size.height - 6.0 + (isSearchActive ? 40.0 : 0.0)) + ) + if let keyboardComponentView = self.keyboardView.view { + if keyboardComponentView.superview == nil { + self.keyboardContainerView.insertSubview(keyboardComponentView, at: 0) + } + + self.panelBackgroundView.update(size: CGSize(width: keyboardSize.width, height: 42.0), transition: .immediate) + self.panelBackgroundView.updateColor(color: environment.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.8), transition: .immediate) + + transition.setFrame(view: self.panelBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: isSearchActive ? -42.0 : 0.0), size: CGSize(width: keyboardSize.width, height: 42.0))) + transition.setFrame(view: self.panelHostView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight - 34.0), size: CGSize(width: keyboardSize.width, height: 0.0))) + transition.setFrame(view: keyboardComponentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight - 34.0), size: keyboardSize)) + + transition.setFrame(view: self.panelSeparatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: isSearchActive ? -UIScreenPixel : topPanelHeight), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + transition.setAlpha(view: self.panelSeparatorView, alpha: isSearchActive ? 0.0 : 1.0) + } + } + + contentHeight += keyboardContainerFrame.height + + if effectiveIsExpanded { + contentHeight += bottomInset + } else { + contentHeight += 16.0 + } + + let buttonSize = self.buttonView.update( + transition: transition, + component: AnyComponent( + SolidRoundedButtonComponent( + title: "Set Video", + theme: SolidRoundedButtonComponent.Theme(theme: environment.theme), + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + action: { [weak self] in + self?.complete() + } + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: environment.navigationHeight - environment.statusBarHeight) + ) + if let buttonView = self.buttonView.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize)) + } + + return availableSize + } + + func complete() { + guard let state = self.state, let item = state.selectedItem, let itemFile = item.itemFile, let previewView = self.previewView.view else { + return + } + let size = CGSize(width: 1920.0, height: 1920.0) + let image = state.selectedBackground.generateImage(size: size) + let tempPath = NSTemporaryDirectory() + "/\(UInt64.random(in: 0 ... UInt64.max)).jpg" + let tempUrl = NSURL(fileURLWithPath: tempPath) as URL + try? image.jpegData(compressionQuality: 1.0)?.write(to: tempUrl) + + let entity = DrawingStickerEntity(content: .file(itemFile)) + entity.referenceDrawingSize = size + entity.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + entity.scale = 3.3 + + let entitiesData = DrawingEntitiesView.encodeEntities([entity]) + + let paintingData = TGPaintingData( + drawing: nil, + entitiesData: entitiesData, + image: nil, + stillImage: nil, + hasAnimation: true, + stickers: [] + ) + + let adjustments = PGPhotoEditorValues( + originalSize: size, + cropRect: CGRect(origin: .zero, size: size), + cropRotation: 0.0, + cropOrientation: .up, + cropLockedAspectRatio: 1.0, + cropMirrored: false, + toolValues: [:], + paintingData: paintingData, + sendAsGif: true + ) + let preset: TGMediaVideoConversionPreset = TGMediaVideoConversionPresetProfileHigh + + let combinedImage = generateImage(previewView.bounds.size, contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + if let cgImage = image.cgImage { + context.draw(cgImage, in: bounds) + } + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + previewView.layer.render(in: context) + }, opaque: false)! + + self.controller?()?.completion(combinedImage, tempUrl, TGVideoEditAdjustments(photoEditorValues: adjustments, preset: preset), { [weak self] in + self?.controller?()?.dismiss() + }) + } + } + + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class AvatarEditorScreen: ViewControllerComponentContainer { + fileprivate let context: AccountContext + + private let readyValue = Promise() + override public var ready: Promise { + return self.readyValue + } + + public var completion: (UIImage, URL, TGVideoEditAdjustments, @escaping () -> Void) -> Void = { _, _, _, _ in } + + public init(context: AccountContext) { + self.context = context + + let componentReady = Promise() + super.init(context: context, component: AvatarEditorScreenComponent(context: context, ready: componentReady), navigationBarAppearance: .transparent) + self.navigationPresentation = .modal + + self.readyValue.set(componentReady.get() |> timeout(0.3, queue: .mainQueue(), alternate: .single(true))) + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView()) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidLoad() { + super.viewDidLoad() + } +} diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarPreviewComponent.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarPreviewComponent.swift new file mode 100644 index 0000000000..fe0de627c7 --- /dev/null +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarPreviewComponent.swift @@ -0,0 +1,215 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import MultilineTextComponent +import EmojiStatusComponent +import Postbox +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import StickerResources + +final class AvatarPreviewComponent: Component { + typealias EnvironmentType = Empty + + let context: AccountContext + let background: AvatarBackground + let file: TelegramMediaFile? + let tapped: () -> Void + + init( + context: AccountContext, + background: AvatarBackground, + file: TelegramMediaFile?, + tapped: @escaping () -> Void + ) { + self.context = context + self.background = background + self.file = file + self.tapped = tapped + } + + static func ==(lhs: AvatarPreviewComponent, rhs: AvatarPreviewComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.background != rhs.background { + return false + } + if lhs.file != rhs.file { + return false + } + return true + } + + final class View: UIView, UITextFieldDelegate { + private let imageView: UIImageView + + private let imageNode: TransformImageNode + private var animationNode: AnimatedStickerNode? + + private var component: AvatarPreviewComponent? + private weak var state: EmptyComponentState? + + private let stickerFetchedDisposable = MetaDisposable() + + override init(frame: CGRect) { + self.imageView = UIImageView() + self.imageView.isUserInteractionEnabled = false + + self.imageNode = TransformImageNode() + + super.init(frame: frame) + + self.disablesInteractiveModalDismiss = true + self.disablesInteractiveKeyboardGestureRecognizer = true + + self.addSubview(self.imageView) + + self.addSubnode(self.imageNode) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapped))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.stickerFetchedDisposable.dispose() + } + + @objc func tapped() { + self.animationNode?.playOnce() + self.component?.tapped() + } + + func update(component: AvatarPreviewComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousBackground = self.component?.background + + let hadFile = self.component?.file != nil + var fileUpdated = false + if self.component?.file?.fileId != component.file?.fileId { + self.imageNode.isHidden = false + fileUpdated = true + } + + self.component = component + self.state = state + + let size = CGSize(width: availableSize.width * 0.66, height: availableSize.width * 0.66) + + var dimensions: CGSize? + if let file = component.file, fileUpdated, let fileDimensions = file.dimensions?.cgSize { + dimensions = fileDimensions + + if !self.imageNode.isHidden && hadFile, let snapshotView = self.imageNode.view.snapshotContentTree() { + self.imageNode.view.superview?.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + snapshotView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + + if let animationNode = self.animationNode { + self.animationNode = nil + + animationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + animationNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { [weak animationNode] _ in + animationNode?.removeFromSupernode() + }) + } + + if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" { + self.imageNode.isHidden = false + + if self.animationNode == nil { + let animationNode = DefaultAnimatedStickerNodeImpl() + animationNode.autoplay = false + self.animationNode = animationNode + animationNode.started = { [weak self] in + self?.imageNode.isHidden = true + } + self.addSubnode(animationNode) + } + + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: component.context.account.postbox, userLocation: .other, file: file, small: false, size: fileDimensions.aspectFitted(CGSize(width: 256.0, height: 256.0)))) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: file.resource).start()) + } else { + if let animationNode = self.animationNode { + animationNode.visibility = false + self.animationNode = nil + animationNode.removeFromSupernode() + self.imageNode.isHidden = false + } + self.imageNode.setSignal(chatMessageSticker(account: component.context.account, userLocation: .other, file: file, small: false, synchronousLoad: false)) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: false)).start()) + } + + if fileUpdated && hadFile { + self.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.imageNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + if let animationNode = self.animationNode { + animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + animationNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + } + } + } + + if let dimensions { + let imageSize = dimensions.aspectFitted(size) + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() + self.imageNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - imageSize.width) / 2.0), y: (availableSize.height - imageSize.height) / 2.0), size: imageSize) + + if let animationNode = self.animationNode { + animationNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - imageSize.width) / 2.0), y: (availableSize.height - imageSize.height) / 2.0), size: imageSize) + animationNode.updateLayout(size: imageSize) + } + + if fileUpdated { + self.updateVisibility() + } + } + + self.imageView.frame = CGRect(origin: .zero, size: availableSize) + if previousBackground != component.background { + if let _ = previousBackground, !transition.animation.isImmediate, let snapshotView = self.imageView.snapshotContentTree() { + self.insertSubview(snapshotView, aboveSubview: self.imageView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + self.imageView.image = component.background.generateImage(size: availableSize) + } + + return availableSize + } + + private func updateVisibility() { + guard let component = self.component, let file = component.file else { + return + } + let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) + let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)) + let source = AnimatedStickerResourceSource(account: component.context.account, resource: file.resource, isVideo: file.isVideoSticker || file.mimeType == "video/webm") + self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .count(2), mode: .direct(cachePathPrefix: nil)) + self.animationNode?.visibility = true + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/BackgroundColorComponent.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/BackgroundColorComponent.swift new file mode 100644 index 0000000000..90190a4217 --- /dev/null +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/BackgroundColorComponent.swift @@ -0,0 +1,313 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData + +final class BackgroundColorComponent: Component { + let theme: PresentationTheme + let values: [AvatarBackground] + let selectedValue: AvatarBackground + let updateValue: (AvatarBackground) -> Void + let openColorPicker: () -> Void + + init( + theme: PresentationTheme, + values: [AvatarBackground], + selectedValue: AvatarBackground, + updateValue: @escaping (AvatarBackground) -> Void, + openColorPicker: @escaping () -> Void + ) { + self.theme = theme + self.values = values + self.selectedValue = selectedValue + self.updateValue = updateValue + self.openColorPicker = openColorPicker + } + + static func ==(lhs: BackgroundColorComponent, rhs: BackgroundColorComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.values != rhs.values { + return false + } + if lhs.selectedValue != rhs.selectedValue { + return false + } + return true + } + + class View: UIView { + private var views: [Int: ComponentView] = [:] + + private var component: BackgroundColorComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.clipsToBounds = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BackgroundColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + var values: [(AvatarBackground?, Bool)] = component.values.map { ($0, false) } + if !values.contains(where: { $0.0 == component.selectedValue }) { + values.append((component.selectedValue, true)) + } else { + values.append((nil, true)) + } + + let itemSize = CGSize(width: 30.0, height: 30.0) + let sideInset: CGFloat = 12.0 + let height: CGFloat = 50.0 + let delta = (availableSize.width - sideInset * 2.0 - CGFloat(values.count) * itemSize.width) / CGFloat(values.count - 1) + + for i in 0 ..< values.count { + let view: ComponentView + if let current = self.views[i] { + view = current + } else { + view = ComponentView() + self.views[i] = view + } + + let itemSize = view.update( + transition: transition, + component: AnyComponent( + BackgroundSwatchComponent( + theme: component.theme, + background: values[i].0, + isCustom: values[i].1, + isSelected: component.selectedValue == values[i].0, + action: { + if !values[i].1, let value = values[i].0 { + component.updateValue(value) + } else { + component.openColorPicker() + } + } + ) + ), + environment: {}, + containerSize: itemSize + ) + if let itemView = view.view { + if itemView.superview == nil { + self.addSubview(itemView) + } + + let position: CGFloat = sideInset + (delta + itemSize.width) * CGFloat(i) + transition.setFrame(view: itemView, frame: CGRect(origin: CGPoint(x: position, y: 10.0), size: itemSize)) + } + } + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private func generateAddIcon(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + context.setStrokeColor(color.cgColor) + context.setLineWidth(2.0) + context.setLineCap(.round) + + context.move(to: CGPoint(x: 15.0, y: 9.0)) + context.addLine(to: CGPoint(x: 15.0, y: 21.0)) + context.strokePath() + + context.move(to: CGPoint(x: 9.0, y: 15.0)) + context.addLine(to: CGPoint(x: 21.0, y: 15.0)) + context.strokePath() + }) +} + +private func generateMoreIcon() -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + context.setFillColor(UIColor.white.cgColor) + + context.addEllipse(in: CGRect(x: 8.5, y: 13.5, width: 3.0, height: 3.0)) + context.fillPath() + + context.addEllipse(in: CGRect(x: 13.5, y: 13.5, width: 3.0, height: 3.0)) + context.fillPath() + + context.addEllipse(in: CGRect(x: 18.5, y: 13.5, width: 3.0, height: 3.0)) + context.fillPath() + }) +} + +final class BackgroundSwatchComponent: Component { + let theme: PresentationTheme + let background: AvatarBackground? + let isCustom: Bool + let isSelected: Bool + let action: () -> Void + + init( + theme: PresentationTheme, + background: AvatarBackground?, + isCustom: Bool, + isSelected: Bool, + action: @escaping () -> Void + ) { + self.theme = theme + self.background = background + self.isCustom = isCustom + self.isSelected = isSelected + self.action = action + } + + static func == (lhs: BackgroundSwatchComponent, rhs: BackgroundSwatchComponent) -> Bool { + return lhs.theme === rhs.theme && lhs.background == rhs.background && lhs.isCustom == rhs.isCustom && lhs.isSelected == rhs.isSelected + } + + final class View: UIButton { + private var component: BackgroundSwatchComponent? + + private let maskLayer: SimpleLayer + private let ringMaskLayer: SimpleShapeLayer + private let circleMaskLayer: SimpleShapeLayer + + private let iconLayer: SimpleLayer + + private var currentIsHighlighted: Bool = false { + didSet { + if self.currentIsHighlighted != oldValue { + self.alpha = self.currentIsHighlighted ? 0.6 : 1.0 + } + } + } + + override init(frame: CGRect) { + self.maskLayer = SimpleLayer() + self.ringMaskLayer = SimpleShapeLayer() + self.circleMaskLayer = SimpleShapeLayer() + self.iconLayer = SimpleLayer() + + super.init(frame: frame) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + self.component?.action() + } + + override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + self.currentIsHighlighted = true + + return super.beginTracking(touch, with: event) + } + + override public func endTracking(_ touch: UITouch?, with event: UIEvent?) { + self.currentIsHighlighted = false + + super.endTracking(touch, with: event) + } + + override public func cancelTracking(with event: UIEvent?) { + self.currentIsHighlighted = false + + super.cancelTracking(with: event) + } + + func update(component: BackgroundSwatchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousBackground = self.component?.background + self.component = component + + let contentSize = availableSize + let bounds = CGRect(origin: .zero, size: contentSize) + + self.layer.allowsGroupOpacity = true + + if self.layer.mask == nil { + self.layer.mask = self.maskLayer + self.maskLayer.frame = bounds + + self.maskLayer.addSublayer(self.circleMaskLayer) + self.maskLayer.addSublayer(self.ringMaskLayer) + + self.circleMaskLayer.frame = bounds + if self.circleMaskLayer.path == nil { + self.circleMaskLayer.path = UIBezierPath(ovalIn: bounds.insetBy(dx: 3.0, dy: 3.0)).cgPath + } + + let ringFrame = bounds + self.ringMaskLayer.frame = CGRect(origin: .zero, size: ringFrame.size) + self.ringMaskLayer.strokeColor = UIColor.white.cgColor + self.ringMaskLayer.fillColor = UIColor.clear.cgColor + self.ringMaskLayer.lineWidth = 2.0 - UIScreenPixel + self.ringMaskLayer.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: ringFrame.size).insetBy(dx: 1.0, dy: 1.0)).cgPath + + self.layer.addSublayer(self.iconLayer) + } + + self.iconLayer.frame = bounds + if component.isCustom { + if previousBackground != component.background || self.iconLayer.contents == nil { + if component.background != nil { + self.iconLayer.contents = generateMoreIcon()?.cgImage + } else { + self.iconLayer.contents = generateAddIcon(color: component.theme.list.itemAccentColor)?.cgImage + } + } + } else { + self.iconLayer.contents = nil + } + + if component.isSelected { + transition.setShapeLayerPath(layer: self.circleMaskLayer, path: CGPath(ellipseIn: bounds.insetBy(dx: 3.0, dy: 3.0), transform: nil)) + } else { + transition.setShapeLayerPath(layer: self.circleMaskLayer, path: CGPath(ellipseIn: bounds, transform: nil)) + } + + if previousBackground != component.background { + if let background = component.background { + self.layer.backgroundColor = nil + self.layer.contents = background.generateImage(size: availableSize).cgImage + } else { + self.layer.backgroundColor = component.theme.list.itemAccentColor.withAlphaComponent(0.1).cgColor + self.layer.contents = nil + } + } else if component.background == nil { + self.layer.backgroundColor = component.theme.list.itemAccentColor.withAlphaComponent(0.1).cgColor + self.layer.contents = nil + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/ColorPickerComponent.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/ColorPickerComponent.swift new file mode 100644 index 0000000000..e5c25f9aba --- /dev/null +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/ColorPickerComponent.swift @@ -0,0 +1,1283 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import HexColor + +private struct WallpaperColorPanelNodeState: Equatable { + var selection: Int? + var colors: [HSBColor] + var maximumNumberOfColors: Int + var preview: Bool + var simpleGradientGeneration: Bool + var suggestedNewColor: HSBColor? +} + +final class ColorPickerComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let colors: [UInt32] + let colorsChanged: ([UInt32]) -> Void + let cancel: () -> Void + let done: () -> Void + + init( + theme: PresentationTheme, + strings: PresentationStrings, + colors: [UInt32], + colorsChanged: @escaping ([UInt32]) -> Void, + cancel: @escaping () -> Void, + done: @escaping () -> Void + ) { + self.theme = theme + self.strings = strings + self.colors = colors + self.colorsChanged = colorsChanged + self.cancel = cancel + self.done = done + } + + static func ==(lhs: ColorPickerComponent, rhs: ColorPickerComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.colors != rhs.colors { + return false + } + return true + } + + final class View: UIView, UIGestureRecognizerDelegate { + private var state: WallpaperColorPanelNodeState + + private let bottomSeparatorNode: ASDisplayNode + + private let addButton: HighlightableButtonNode + private let colorPickerNode: WallpaperColorPickerNode + + private var sampleItemNodes: [ColorSampleItemNode] = [] + private let multiColorFieldNode: ColorInputFieldNode + + private let cancelHighlightView: UIView + private let cancelButton: HighlightTrackingButton + private let doneHighlightView: UIView + private let doneButton: HighlightTrackingButton + + private let topSeparatorView: UIView + private let separatorView: UIView + + init(theme: PresentationTheme, strings: PresentationStrings) { + self.addButton = HighlightableButtonNode() + + self.colorPickerNode = WallpaperColorPickerNode(strings: strings) + self.multiColorFieldNode = ColorInputFieldNode(theme: theme, displaySwatch: false) + + self.bottomSeparatorNode = ASDisplayNode() + + self.state = WallpaperColorPanelNodeState( + selection: 0, + colors: [], + maximumNumberOfColors: 4, + preview: false, + simpleGradientGeneration: false + ) + + self.cancelHighlightView = UIView() + self.cancelHighlightView.alpha = 0.0 + self.cancelHighlightView.isUserInteractionEnabled = false + + self.cancelButton = HighlightTrackingButton() + self.cancelButton.isExclusiveTouch = true + + self.doneHighlightView = UIView() + self.doneHighlightView.alpha = 0.0 + self.doneHighlightView.isUserInteractionEnabled = false + + self.doneButton = HighlightTrackingButton() + self.doneButton.isExclusiveTouch = true + + self.topSeparatorView = UIView() + self.separatorView = UIView() + + super.init(frame: CGRect()) + + self.layer.allowsGroupOpacity = true + + self.disablesInteractiveModalDismiss = true + self.disablesInteractiveKeyboardGestureRecognizer = true + self.disablesInteractiveTransitionGestureRecognizer = true + + self.addSubnode(self.multiColorFieldNode) + self.addSubnode(self.colorPickerNode) + self.addSubnode(self.addButton) + + self.addSubview(self.cancelHighlightView) + self.addSubview(self.cancelButton) + + self.addSubview(self.doneHighlightView) + self.addSubview(self.doneButton) + + self.addSubview(self.topSeparatorView) + self.addSubview(self.separatorView) + + self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), for: .touchUpInside) + self.doneButton.addTarget(self, action: #selector(self.donePressed), for: .touchUpInside) + + self.cancelButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.cancelHighlightView.layer.removeAnimation(forKey: "opacity") + strongSelf.cancelHighlightView.alpha = 1.0 + } else { + strongSelf.cancelHighlightView.alpha = 0.0 + strongSelf.cancelHighlightView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + } + } + + self.doneButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.doneHighlightView.layer.removeAnimation(forKey: "opacity") + strongSelf.doneHighlightView.alpha = 1.0 + } else { + strongSelf.doneHighlightView.alpha = 0.0 + strongSelf.doneHighlightView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + } + } + + self.addButton.addTarget(self, action: #selector(self.addPressed), forControlEvents: .touchUpInside) + + self.multiColorFieldNode.colorChanged = { [weak self] color, ended in + if let strongSelf = self { + strongSelf.updateState({ current in + var updated = current + updated.preview = !ended + if let index = strongSelf.state.selection { + updated.colors[index] = HSBColor(color: color) + } + return updated + }) + } + } + self.multiColorFieldNode.colorRemoved = { [weak self] in + if let strongSelf = self { + strongSelf.updateState({ current in + var updated = current + if let index = strongSelf.state.selection { + updated.colors.remove(at: index) + if updated.colors.isEmpty { + updated.selection = nil + } else { + updated.selection = max(0, min(index - 1, updated.colors.count - 1)) + } + } + return updated + }, animated: strongSelf.state.colors.count >= 2) + } + } + + self.colorPickerNode.colorChanged = { [weak self] color in + if let strongSelf = self { + strongSelf.updateState({ current in + var updated = current + updated.preview = true + if let index = strongSelf.state.selection { + updated.colors[index] = color + } + return updated + }, updateLayout: false) + } + } + self.colorPickerNode.colorChangeEnded = { [weak self] color in + if let strongSelf = self { + strongSelf.updateState({ current in + var updated = current + updated.preview = false + if let index = strongSelf.state.selection { + updated.colors[index] = color + } + return updated + }, updateLayout: false) + } + } + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + @objc private func cancelPressed() { + self.component?.cancel() + } + + @objc private func donePressed() { + self.component?.done() + } + + @objc private func addPressed() { + self.multiColorFieldNode.setSkipEndEditingIfNeeded() + + self.updateState({ current in + var current = current + if current.colors.count < current.maximumNumberOfColors { + if current.colors.isEmpty { + current.colors.append(HSBColor(rgb: 0xffffff)) + } else if current.simpleGradientGeneration { + var hsb = current.colors[0].values + if hsb.1 > 0.5 { + hsb.1 -= 0.15 + } else { + hsb.1 += 0.15 + } + if hsb.0 > 0.5 { + hsb.0 -= 0.05 + } else { + hsb.0 += 0.05 + } + current.colors.append(HSBColor(values: hsb)) + } else if let suggestedNewColor = current.suggestedNewColor { + current.colors.append(suggestedNewColor) + } else { + current.colors.append(current.colors[current.colors.count - 1]) + } + current.selection = current.colors.count - 1 + } + return current + }) + } + + fileprivate func updateState(_ f: (WallpaperColorPanelNodeState) -> WallpaperColorPanelNodeState, updateLayout: Bool = true, notify: Bool = true, animated: Bool = true) { + var updateLayout = updateLayout + let previousColors = self.state.colors + let previousPreview = self.state.preview + let previousSelection = self.state.selection + self.state = f(self.state) + + let colorWasRemovable = self.multiColorFieldNode.isRemovable + self.multiColorFieldNode.isRemovable = self.state.colors.count > 1 + if colorWasRemovable != self.multiColorFieldNode.isRemovable { + updateLayout = true + } + + if let index = self.state.selection { + if self.state.colors.count > index { + self.colorPickerNode.color = self.state.colors[index] + } + } + + if updateLayout, let size = self.validLayout { + self.updateLayout(size: size, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate) + } + + if let index = self.state.selection { + if self.state.colors.count > index { + self.multiColorFieldNode.setColor(self.state.colors[index].color, update: false) + } + } + + for i in 0 ..< self.state.colors.count { + if i < self.sampleItemNodes.count { + self.sampleItemNodes[i].update(size: self.sampleItemNodes[i].bounds.size, color: self.state.colors[i].color, isSelected: state.selection == i) + } + } + + if notify && (self.state.colors != previousColors || self.state.preview != previousPreview || self.state.selection != previousSelection) { + self.component?.colorsChanged(self.state.colors.map { $0.rgb }) + } + } + + private var validLayout: CGSize? + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = size + + let separatorHeight = UIScreenPixel + let topPanelHeight: CGFloat = 50.0 + transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(x: 0.0, y: topPanelHeight, width: size.width, height: separatorHeight)) + + let fieldHeight: CGFloat = 30.0 + let leftInset: CGFloat = 12.0 + let rightInset: CGFloat = 12.0 + + let buttonSize = CGSize(width: 26.0, height: 26.0) + let canAddColors = self.state.colors.count < self.state.maximumNumberOfColors + + transition.updateFrame(node: self.addButton, frame: CGRect(origin: CGPoint(x: size.width - rightInset - buttonSize.width, y: floor((topPanelHeight - buttonSize.height) / 2.0)), size: buttonSize)) + transition.updateAlpha(node: self.addButton, alpha: canAddColors ? 1.0 : 0.0) + transition.updateSublayerTransformScale(node: self.addButton, scale: canAddColors ? 1.0 : 0.1) + + self.multiColorFieldNode.isHidden = false + + let sampleItemSize: CGFloat = 30.0 + let sampleItemSpacing: CGFloat = 10.0 + + var nextSampleX = leftInset + + for i in 0 ..< self.state.colors.count { + var animateIn = false + let itemNode: ColorSampleItemNode + if self.sampleItemNodes.count > i { + itemNode = self.sampleItemNodes[i] + } else { + itemNode = ColorSampleItemNode(action: { [weak self] in + guard let strongSelf = self else { + return + } + let index = i + strongSelf.updateState({ state in + var state = state + state.selection = index + return state + }) + }) + self.sampleItemNodes.append(itemNode) + self.insertSubview(itemNode.view, aboveSubview: self.multiColorFieldNode.view) + animateIn = true + } + + if i != 0 { + nextSampleX += sampleItemSpacing + } + itemNode.frame = CGRect(origin: CGPoint(x: nextSampleX, y: (topPanelHeight - sampleItemSize) / 2.0), size: CGSize(width: sampleItemSize, height: sampleItemSize)) + nextSampleX += sampleItemSize + itemNode.update(size: itemNode.bounds.size, color: self.state.colors[i].color, isSelected: self.state.selection == i) + + if animateIn { + transition.animateTransformScale(node: itemNode, from: 0.1) + itemNode.alpha = 0.0 + transition.updateAlpha(node: itemNode, alpha: 1.0) + } + } + if self.sampleItemNodes.count > self.state.colors.count { + for i in self.state.colors.count ..< self.sampleItemNodes.count { + let itemNode = self.sampleItemNodes[i] + transition.updateTransformScale(node: itemNode, scale: 0.1) + transition.updateAlpha(node: itemNode, alpha: 0.0, completion: { [weak itemNode] _ in + itemNode?.removeFromSupernode() + }) + } + self.sampleItemNodes.removeSubrange(self.state.colors.count ..< self.sampleItemNodes.count) + } + + let fieldX = nextSampleX + sampleItemSpacing + + let fieldFrame = CGRect(x: fieldX, y: (topPanelHeight - fieldHeight) / 2.0, width: size.width - fieldX - leftInset - (canAddColors ? (buttonSize.width + sampleItemSpacing) : 0.0), height: fieldHeight) + transition.updateFrame(node: self.multiColorFieldNode, frame: fieldFrame) + self.multiColorFieldNode.updateLayout(size: fieldFrame.size, condensed: false, transition: transition) + + let colorPickerSize = CGSize(width: size.width, height: size.height - topPanelHeight - separatorHeight) + transition.updateFrame(node: self.colorPickerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight + separatorHeight), size: colorPickerSize)) + self.colorPickerNode.updateLayout(size: colorPickerSize, transition: transition) + } + + private var component: ColorPickerComponent? + func update(component: ColorPickerComponent, availableSize: CGSize, transition: Transition) -> CGSize { + let themeChanged = self.component?.theme !== component.theme + self.component = component + + let buttonHeight: CGFloat = 44.0 + let size = CGSize(width: availableSize.width, height: 264.0) + let panelSize = CGSize(width: availableSize.width, height: size.height + buttonHeight) + + if themeChanged { + self.addButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorAddIcon"), color: component.theme.chat.inputPanel.panelControlColor), for: .normal) + + self.separatorView.backgroundColor = component.theme.rootController.tabBar.separatorColor + self.topSeparatorView.backgroundColor = component.theme.rootController.tabBar.separatorColor + self.cancelHighlightView.backgroundColor = component.theme.list.itemHighlightedBackgroundColor + self.doneHighlightView.backgroundColor = component.theme.list.itemHighlightedBackgroundColor + self.bottomSeparatorNode.backgroundColor = component.theme.chat.inputPanel.panelSeparatorColor + + self.cancelButton.setAttributedTitle(NSAttributedString(string: component.strings.Common_Cancel, font: Font.regular(17.0), textColor: component.theme.list.itemAccentColor), for: []) + self.doneButton.setAttributedTitle(NSAttributedString(string: component.strings.Common_Done, font: Font.semibold(17.0), textColor: component.theme.list.itemAccentColor), for: []) + } + + transition.setFrame(view: self.cancelButton, frame: CGRect(x: 0.0, y: size.height, width: availableSize.width / 2.0, height: buttonHeight)) + transition.setFrame(view: self.cancelHighlightView, frame: CGRect(x: 0.0, y: size.height, width: availableSize.width / 2.0, height: buttonHeight)) + + transition.setFrame(view: self.doneButton, frame: CGRect(x: availableSize.width / 2.0, y: size.height, width: availableSize.width / 2.0, height: buttonHeight)) + transition.setFrame(view: self.doneHighlightView, frame: CGRect(x: availableSize.width / 2.0, y: size.height, width: availableSize.width / 2.0, height: buttonHeight)) + + transition.setFrame(view: self.topSeparatorView, frame: CGRect(x: 0.0, y: size.height, width: availableSize.width, height: UIScreenPixel)) + transition.setFrame(view: self.separatorView, frame: CGRect(x: size.width / 2.0, y: size.height, width: UIScreenPixel, height: buttonHeight)) + + self.updateState({ current in + var updated = current + updated.colors = component.colors.map { HSBColor(rgb: $0) } + return updated + }, updateLayout: true, notify: false, animated: false) + + self.updateLayout(size: size, transition: transition.containedViewLayoutTransition) + return panelSize + } + } + + func makeView() -> View { + return View(theme: self.theme, strings: self.strings) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} + +private let knobBackgroundImage: UIImage? = { + return generateImage(CGSize(width: 45.0, height: 45.0), contextGenerator: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setShadow(offset: CGSize(width: 0.0, height: -1.5), blur: 4.5, color: UIColor(rgb: 0x000000, alpha: 0.4).cgColor) + context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.4).cgColor) + context.fillEllipse(in: bounds.insetBy(dx: 3.0 + UIScreenPixel, dy: 3.0 + UIScreenPixel)) + + context.setBlendMode(.normal) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: bounds.insetBy(dx: 3.0, dy: 3.0)) + }, opaque: false, scale: nil) +}() + +private let pointerImage: UIImage? = { + return generateImage(CGSize(width: 12.0, height: 55.0), opaque: false, scale: nil, rotatedContext: { size, context in + context.setBlendMode(.clear) + context.setFillColor(UIColor.clear.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.normal) + + let lineWidth: CGFloat = 1.0 + context.setFillColor(UIColor.black.cgColor) + context.setStrokeColor(UIColor.white.cgColor) + context.setLineWidth(lineWidth) + context.setLineCap(.round) + context.setLineJoin(.round) + + let pointerHeight: CGFloat = 7.0 + context.move(to: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0)) + context.addLine(to: CGPoint(x: size.width - lineWidth / 2.0, y: lineWidth / 2.0)) + context.addLine(to: CGPoint(x: size.width / 2.0, y: lineWidth / 2.0 + pointerHeight)) + context.closePath() + context.drawPath(using: .fillStroke) + + context.move(to: CGPoint(x: lineWidth / 2.0, y: size.height - lineWidth / 2.0)) + context.addLine(to: CGPoint(x: size.width / 2.0, y: size.height - lineWidth / 2.0 - pointerHeight)) + context.addLine(to: CGPoint(x: size.width - lineWidth / 2.0, y: size.height - lineWidth / 2.0)) + context.closePath() + context.drawPath(using: .fillStroke) + }) +}() + +private let brightnessMaskImage: UIImage? = { + return generateImage(CGSize(width: 36.0, height: 36.0), opaque: false, scale: nil, rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + + context.setFillColor(UIColor.white.cgColor) + context.fill(bounds) + + context.setBlendMode(.clear) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: bounds) + })?.stretchableImage(withLeftCapWidth: 18, topCapHeight: 18) +}() + +private let brightnessGradientImage: UIImage? = { + return generateImage(CGSize(width: 160.0, height: 1.0), opaque: false, scale: nil, rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let gradientColors = [UIColor.black.withAlphaComponent(0.0), UIColor.black].map { $0.cgColor } as CFArray + var locations: [CGFloat] = [0.0, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + }) +}() + +private final class HSBParameter: NSObject { + let hue: CGFloat + let saturation: CGFloat + let value: CGFloat + + init(hue: CGFloat, saturation: CGFloat, value: CGFloat) { + self.hue = hue + self.saturation = saturation + self.value = value + super.init() + } +} + +private final class WallpaperColorKnobNode: ASDisplayNode { + var color: HSBColor = HSBColor(hue: 0.0, saturation: 0.0, brightness: 1.0) { + didSet { + if self.color != oldValue { + self.colorNode.backgroundColor = self.color.color + } + } + } + + private let backgroundNode: ASImageNode + private let colorNode: ASDisplayNode + + override init() { + self.backgroundNode = ASImageNode() + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.image = knobBackgroundImage + + self.colorNode = ASDisplayNode() + + super.init() + + self.isUserInteractionEnabled = false + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.colorNode) + } + + override func layout() { + super.layout() + + self.backgroundNode.frame = self.bounds + self.colorNode.frame = self.bounds.insetBy(dx: 7.0 - UIScreenPixel, dy: 7.0 - UIScreenPixel) + self.colorNode.cornerRadius = self.colorNode.frame.width / 2.0 + } +} + +private final class WallpaperColorHueSaturationNode: ASDisplayNode { + var value: CGFloat = 1.0 { + didSet { + if self.value != oldValue { + self.setNeedsDisplay() + } + } + } + + override init() { + super.init() + + self.isOpaque = true + self.displaysAsynchronously = false + } + + override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + return HSBParameter(hue: 1.0, saturation: 1.0, value: 1.0) + } + + @objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + guard let parameters = parameters as? HSBParameter else { + return + } + let context = UIGraphicsGetCurrentContext()! + let colorSpace = CGColorSpaceCreateDeviceRGB() + + let colors = [UIColor(rgb: 0xff0000).cgColor, UIColor(rgb: 0xffff00).cgColor, UIColor(rgb: 0x00ff00).cgColor, UIColor(rgb: 0x00ffff).cgColor, UIColor(rgb: 0x0000ff).cgColor, UIColor(rgb: 0xff00ff).cgColor, UIColor(rgb: 0xff0000).cgColor] + var locations: [CGFloat] = [0.0, 0.16667, 0.33333, 0.5, 0.66667, 0.83334, 1.0] + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: bounds.width, y: 0.0), options: CGGradientDrawingOptions()) + + let overlayColors = [UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, UIColor(rgb: 0xffffff).cgColor] + var overlayLocations: [CGFloat] = [0.0, 1.0] + let overlayGradient = CGGradient(colorsSpace: colorSpace, colors: overlayColors as CFArray, locations: &overlayLocations)! + context.drawLinearGradient(overlayGradient, start: CGPoint(), end: CGPoint(x: 0.0, y: bounds.height), options: CGGradientDrawingOptions()) + + context.setFillColor(UIColor(rgb: 0x000000, alpha: 1.0 - parameters.value).cgColor) + context.fill(bounds) + } + + var tap: ((CGPoint) -> Void)? + var panBegan: ((CGPoint) -> Void)? + var panChanged: ((CGPoint, Bool) -> Void)? + + var initialTouchLocation: CGPoint? + var touchMoved = false + var previousTouchLocation: CGPoint? + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + + if let touchLocation = touches.first?.location(in: self.view) { + self.touchMoved = false + self.initialTouchLocation = touchLocation + self.previousTouchLocation = nil + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent?) { + super.touchesMoved(touches, with: event) + + if let touchLocation = touches.first?.location(in: self.view), let initialLocation = self.initialTouchLocation { + let dX = touchLocation.x - initialLocation.x + let dY = touchLocation.y - initialLocation.y + if !self.touchMoved && dX * dX + dY * dY > 3.0 { + self.touchMoved = true + self.panBegan?(touchLocation) + self.previousTouchLocation = touchLocation + } else if let previousTouchLocation = self.previousTouchLocation { + let dX = touchLocation.x - previousTouchLocation.x + let dY = touchLocation.y - previousTouchLocation.y + let translation = CGPoint(x: dX, y: dY) + + self.panChanged?(translation, false) + self.previousTouchLocation = touchLocation + } + } + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + super.touchesEnded(touches, with: event) + + if self.touchMoved { + if let touchLocation = touches.first?.location(in: self.view), let previousTouchLocation = self.previousTouchLocation { + let dX = touchLocation.x - previousTouchLocation.x + let dY = touchLocation.y - previousTouchLocation.y + let translation = CGPoint(x: dX, y: dY) + + self.panChanged?(translation, true) + } + } else if let touchLocation = self.initialTouchLocation { + self.tap?(touchLocation) + } + } + + override func touchesCancelled(_ touches: Set?, with event: UIEvent?) { + super.touchesCancelled(touches, with: event) + } +} + +private final class WallpaperColorBrightnessNode: ASDisplayNode { + private let gradientNode: ASImageNode + private let maskNode: ASImageNode + + var hsb: (CGFloat, CGFloat, CGFloat) = (0.0, 1.0, 1.0) { + didSet { + if self.hsb.0 != oldValue.0 || self.hsb.1 != oldValue.1 { + let color = UIColor(hue: hsb.0, saturation: hsb.1, brightness: 1.0, alpha: 1.0) + self.backgroundColor = color + } + } + } + + override init() { + self.gradientNode = ASImageNode() + self.gradientNode.displaysAsynchronously = false + self.gradientNode.displayWithoutProcessing = true + self.gradientNode.image = brightnessGradientImage + self.gradientNode.contentMode = .scaleToFill + + self.maskNode = ASImageNode() + self.maskNode.displaysAsynchronously = false + self.maskNode.displayWithoutProcessing = true + self.maskNode.image = brightnessMaskImage + self.maskNode.contentMode = .scaleToFill + + super.init() + + self.isOpaque = true + self.addSubnode(self.gradientNode) + self.addSubnode(self.maskNode) + } + + override func layout() { + super.layout() + + self.gradientNode.frame = self.bounds + self.maskNode.frame = self.bounds + } +} + +struct HSBColor: Equatable { + static func == (lhs: HSBColor, rhs: HSBColor) -> Bool { + return lhs.values.h == rhs.values.h && lhs.values.s == rhs.values.s && lhs.values.b == rhs.values.b + } + + let values: (h: CGFloat, s: CGFloat, b: CGFloat) + let backingColor: UIColor + + var hue: CGFloat { + return self.values.h + } + + var saturation: CGFloat { + return self.values.s + } + + var brightness: CGFloat { + return self.values.b + } + + var rgb: UInt32 { + return self.color.argb + } + + init(values: (h: CGFloat, s: CGFloat, b: CGFloat)) { + self.values = values + self.backingColor = UIColor(hue: values.h, saturation: values.s, brightness: values.b, alpha: 1.0) + } + + init(hue: CGFloat, saturation: CGFloat, brightness: CGFloat) { + self.values = (h: hue, s: saturation, b: brightness) + self.backingColor = UIColor(hue: self.values.h, saturation: self.values.s, brightness: self.values.b, alpha: 1.0) + } + + init(color: UIColor) { + self.values = color.hsb + self.backingColor = color + } + + init(rgb: UInt32) { + self.init(color: UIColor(rgb: rgb)) + } + + var color: UIColor { + return self.backingColor + } +} + +final class WallpaperColorPickerNode: ASDisplayNode { + private let brightnessNode: WallpaperColorBrightnessNode + private let brightnessKnobNode: ASImageNode + private let colorNode: WallpaperColorHueSaturationNode + private let colorKnobNode: WallpaperColorKnobNode + + private var validLayout: CGSize? + + var color: HSBColor = HSBColor(hue: 0.0, saturation: 1.0, brightness: 1.0) { + didSet { + if self.color != oldValue { + self.update() + } + } + } + + var colorChanged: ((HSBColor) -> Void)? + var colorChangeEnded: ((HSBColor) -> Void)? + + init(strings: PresentationStrings) { + self.brightnessNode = WallpaperColorBrightnessNode() + self.brightnessNode.hitTestSlop = UIEdgeInsets(top: -16.0, left: -16.0, bottom: -16.0, right: -16.0) + self.brightnessKnobNode = ASImageNode() + self.brightnessKnobNode.image = pointerImage + self.brightnessKnobNode.isUserInteractionEnabled = false + self.colorNode = WallpaperColorHueSaturationNode() + self.colorNode.hitTestSlop = UIEdgeInsets(top: -16.0, left: -16.0, bottom: -16.0, right: -16.0) + self.colorKnobNode = WallpaperColorKnobNode() + + super.init() + + self.backgroundColor = .white + + self.addSubnode(self.brightnessNode) + self.addSubnode(self.brightnessKnobNode) + self.addSubnode(self.colorNode) + self.addSubnode(self.colorKnobNode) + + self.update() + + self.colorNode.tap = { [weak self] location in + guard let strongSelf = self, let size = strongSelf.validLayout else { + return + } + + let colorHeight = size.height - 66.0 + + let newHue = max(0.0, min(1.0, location.x / size.width)) + let newSaturation = max(0.0, min(1.0, (1.0 - location.y / colorHeight))) + strongSelf.color = HSBColor(hue: newHue, saturation: newSaturation, brightness: strongSelf.color.brightness) + + strongSelf.updateKnobLayout(size: size, panningColor: false, transition: .immediate) + + strongSelf.update() + strongSelf.colorChangeEnded?(strongSelf.color) + } + + self.colorNode.panBegan = { [weak self] location in + guard let strongSelf = self, let size = strongSelf.validLayout else { + return + } + + let previousColor = strongSelf.color + + let colorHeight = size.height - 66.0 + + let newHue = max(0.0, min(1.0, location.x / size.width)) + let newSaturation = max(0.0, min(1.0, (1.0 - location.y / colorHeight))) + strongSelf.color = HSBColor(hue: newHue, saturation: newSaturation, brightness: strongSelf.color.brightness) + + strongSelf.updateKnobLayout(size: size, panningColor: true, transition: .immediate) + + if strongSelf.color != previousColor { + strongSelf.colorChanged?(strongSelf.color) + } + } + + self.colorNode.panChanged = { [weak self] translation, ended in + guard let strongSelf = self, let size = strongSelf.validLayout else { + return + } + + let previousColor = strongSelf.color + + let colorHeight = size.height - 66.0 + + let newHue = max(0.0, min(1.0, strongSelf.color.hue + translation.x / size.width)) + let newSaturation = max(0.0, min(1.0, strongSelf.color.saturation - translation.y / colorHeight)) + strongSelf.color = HSBColor(hue: newHue, saturation: newSaturation, brightness: strongSelf.color.brightness) + + if ended { + strongSelf.updateKnobLayout(size: size, panningColor: false, transition: .animated(duration: 0.3, curve: .easeInOut)) + } else { + strongSelf.updateKnobLayout(size: size, panningColor: true, transition: .immediate) + } + + if strongSelf.color != previousColor || ended { + strongSelf.update() + if ended { + strongSelf.colorChangeEnded?(strongSelf.color) + } else { + strongSelf.colorChanged?(strongSelf.color) + } + } + } + } + + override func didLoad() { + super.didLoad() + + self.view.disablesInteractiveTransitionGestureRecognizer = true + self.view.disablesInteractiveModalDismiss = true + + let brightnessPanRecognizer = UIPanGestureRecognizer(target: self, action: #selector(WallpaperColorPickerNode.brightnessPan)) + self.brightnessNode.view.addGestureRecognizer(brightnessPanRecognizer) + } + + private func update() { + self.backgroundColor = .white + self.colorNode.value = self.color.brightness + self.brightnessNode.hsb = self.color.values + self.colorKnobNode.color = self.color + } + + private func updateKnobLayout(size: CGSize, panningColor: Bool, transition: ContainedViewLayoutTransition) { + let knobSize = CGSize(width: 45.0, height: 45.0) + + let colorHeight = size.height - 66.0 + var colorKnobFrame = CGRect(x: floorToScreenPixels(-knobSize.width / 2.0 + size.width * self.color.hue), y: floorToScreenPixels(-knobSize.height / 2.0 + (colorHeight * (1.0 - self.color.saturation))), width: knobSize.width, height: knobSize.height) + var origin = colorKnobFrame.origin + if !panningColor { + origin = CGPoint(x: max(0.0, min(origin.x, size.width - knobSize.width)), y: max(0.0, min(origin.y, colorHeight - knobSize.height))) + } else { + origin = origin.offsetBy(dx: 0.0, dy: -32.0) + } + colorKnobFrame.origin = origin + transition.updateFrame(node: self.colorKnobNode, frame: colorKnobFrame) + + let inset: CGFloat = 15.0 + let brightnessKnobSize = CGSize(width: 12.0, height: 55.0) + let brightnessKnobFrame = CGRect(x: inset - brightnessKnobSize.width / 2.0 + (size.width - inset * 2.0) * (1.0 - self.color.brightness), y: size.height - 65.0, width: brightnessKnobSize.width, height: brightnessKnobSize.height) + transition.updateFrame(node: self.brightnessKnobNode, frame: brightnessKnobFrame) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = size + + let colorHeight = size.height - 66.0 + transition.updateFrame(node: self.colorNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: colorHeight)) + + let inset: CGFloat = 15.0 + transition.updateFrame(node: self.brightnessNode, frame: CGRect(x: inset, y: size.height - 55.0, width: size.width - inset * 2.0, height: 35.0)) + + self.updateKnobLayout(size: size, panningColor: false, transition: .immediate) + } + + @objc private func brightnessPan(_ recognizer: UIPanGestureRecognizer) { + guard let size = self.validLayout else { + return + } + + let previousColor = self.color + + let transition = recognizer.translation(in: recognizer.view) + let brightnessWidth: CGFloat = size.width - 42.0 * 2.0 + let newValue = max(0.0, min(1.0, self.color.brightness - transition.x / brightnessWidth)) + self.color = HSBColor(hue: self.color.hue, saturation: self.color.saturation, brightness: newValue) + + var ended = false + switch recognizer.state { + case .changed: + self.updateKnobLayout(size: size, panningColor: false, transition: .immediate) + recognizer.setTranslation(CGPoint(), in: recognizer.view) + case .ended: + self.updateKnobLayout(size: size, panningColor: false, transition: .immediate) + ended = true + default: + break + } + + if self.color != previousColor || ended { + self.update() + if ended { + self.colorChangeEnded?(self.color) + } else { + self.colorChanged?(self.color) + } + } + } +} + +private var currentTextInputBackgroundImage: (UIColor, CGFloat, UIImage)? +private func textInputBackgroundImage(fieldColor: UIColor, diameter: CGFloat) -> UIImage? { + if let current = currentTextInputBackgroundImage { + if current.0.isEqual(fieldColor) && current.1.isEqual(to: diameter) { + return current.2 + } + } + + let image = generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in + context.clear(CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) + context.setFillColor(fieldColor.cgColor) + context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) + })?.stretchableImage(withLeftCapWidth: Int(diameter) / 2, topCapHeight: Int(diameter) / 2) + if let image = image { + currentTextInputBackgroundImage = (fieldColor, diameter, image) + return image + } else { + return nil + } +} + +private func generateSwatchBorderImage(theme: PresentationTheme) -> UIImage? { + return nil +} + +private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { + private var theme: PresentationTheme + + private let swatchNode: ASDisplayNode + private let borderNode: ASImageNode + private let removeButton: HighlightableButtonNode + private let textBackgroundNode: ASImageNode + private let selectionNode: ASDisplayNode + let textFieldNode: TextFieldNode + private let measureNode: ImmediateTextNode + private let prefixNode: ASTextNode + + private var gestureRecognizer: UITapGestureRecognizer? + + var colorChanged: ((UIColor, Bool) -> Void)? + var colorRemoved: (() -> Void)? + var colorSelected: (() -> Void)? + + private var color: UIColor? + + private var isDefault = false { + didSet { + self.updateSelectionVisibility() + } + } + + var isRemovable: Bool = false { + didSet { + self.removeButton.isUserInteractionEnabled = self.isRemovable + } + } + + private var previousIsDefault: Bool? + private var previousColor: UIColor? + private var validLayout: (CGSize, Bool)? + + private var skipEndEditing = false + + private let displaySwatch: Bool + + init(theme: PresentationTheme, displaySwatch: Bool = true) { + self.theme = theme + + self.displaySwatch = displaySwatch + + self.textBackgroundNode = ASImageNode() + self.textBackgroundNode.image = textInputBackgroundImage(fieldColor: theme.list.itemInputField.backgroundColor, diameter: 30.0) + self.textBackgroundNode.displayWithoutProcessing = true + self.textBackgroundNode.displaysAsynchronously = false + + self.selectionNode = ASDisplayNode() + self.selectionNode.backgroundColor = theme.chat.inputPanel.panelControlAccentColor.withAlphaComponent(0.2) + self.selectionNode.cornerRadius = 3.0 + self.selectionNode.isUserInteractionEnabled = false + + self.textFieldNode = TextFieldNode() + self.measureNode = ImmediateTextNode() + + self.prefixNode = ASTextNode() + self.prefixNode.attributedText = NSAttributedString(string: "#", font: Font.regular(17.0), textColor: self.theme.chat.inputPanel.inputTextColor) + + self.swatchNode = ASDisplayNode() + self.swatchNode.cornerRadius = 10.5 + + self.borderNode = ASImageNode() + self.borderNode.displaysAsynchronously = false + self.borderNode.displayWithoutProcessing = true + self.borderNode.image = generateSwatchBorderImage(theme: theme) + + self.removeButton = HighlightableButtonNode() + self.removeButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorRemoveIcon"), color: theme.chat.inputPanel.inputControlColor), for: .normal) + + super.init() + + self.addSubnode(self.textBackgroundNode) + self.addSubnode(self.selectionNode) + self.addSubnode(self.textFieldNode) + self.addSubnode(self.prefixNode) + self.addSubnode(self.swatchNode) + self.addSubnode(self.borderNode) + self.addSubnode(self.removeButton) + + self.removeButton.addTarget(self, action: #selector(self.removePressed), forControlEvents: .touchUpInside) + } + + override func didLoad() { + super.didLoad() + + self.textFieldNode.textField.font = Font.regular(17.0) + self.textFieldNode.textField.textColor = self.theme.chat.inputPanel.inputTextColor + self.textFieldNode.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance + self.textFieldNode.textField.autocorrectionType = .no + self.textFieldNode.textField.autocapitalizationType = .allCharacters + self.textFieldNode.textField.keyboardType = .asciiCapable + self.textFieldNode.textField.returnKeyType = .done + self.textFieldNode.textField.delegate = self + self.textFieldNode.textField.addTarget(self, action: #selector(self.textFieldTextChanged(_:)), for: .editingChanged) + self.textFieldNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) + self.textFieldNode.textField.tintColor = self.theme.list.itemAccentColor + + let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapped(_:))) + self.view.addGestureRecognizer(gestureRecognizer) + self.gestureRecognizer = gestureRecognizer + } + + func updateTheme(_ theme: PresentationTheme) { + self.theme = theme + + self.textBackgroundNode.image = textInputBackgroundImage(fieldColor: self.theme.list.itemInputField.backgroundColor, diameter: 30.0) + + self.textFieldNode.textField.textColor = self.isDefault ? self.theme.chat.inputPanel.inputPlaceholderColor : self.theme.chat.inputPanel.inputTextColor + self.textFieldNode.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance + self.textFieldNode.textField.tintColor = self.theme.list.itemAccentColor + + self.selectionNode.backgroundColor = theme.chat.inputPanel.panelControlAccentColor.withAlphaComponent(0.2) + self.borderNode.image = generateSwatchBorderImage(theme: theme) + self.updateBorderVisibility() + } + + func setColor(_ color: UIColor, isDefault: Bool = false, update: Bool = true, ended: Bool = true) { + self.color = color + self.isDefault = isDefault + let text = color.hexString.uppercased() + self.textFieldNode.textField.text = text + self.textFieldNode.textField.textColor = isDefault ? self.theme.chat.inputPanel.inputPlaceholderColor : self.theme.chat.inputPanel.inputTextColor + if let (size, _) = self.validLayout { + self.updateSelectionLayout(size: size, transition: .immediate) + } + if update { + self.colorChanged?(color, ended) + } + self.swatchNode.backgroundColor = color + self.updateBorderVisibility() + } + + private func updateBorderVisibility() { + guard let color = self.swatchNode.backgroundColor else { + return + } + let inputBackgroundColor = self.theme.chat.inputPanel.inputBackgroundColor + if color.distance(to: inputBackgroundColor) < 200 { + self.borderNode.alpha = 1.0 + } else { + self.borderNode.alpha = 0.0 + } + } + + @objc private func removePressed() { + if self.textFieldNode.textField.isFirstResponder { + self.skipEndEditing = true + } + + self.colorRemoved?() + } + + @objc private func tapped(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.colorSelected?() + } + } + + @objc internal func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + var updated = textField.text ?? "" + updated.replaceSubrange(updated.index(updated.startIndex, offsetBy: range.lowerBound) ..< updated.index(updated.startIndex, offsetBy: range.upperBound), with: string) + if updated.count <= 6 && updated.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789abcdefABCDEF").inverted) == nil { + textField.text = updated.uppercased() + textField.textColor = self.theme.chat.inputPanel.inputTextColor + + if updated.count == 6, let color = UIColor(hexString: updated) { + self.setColor(color) + } + + if let (size, _) = self.validLayout { + self.updateSelectionLayout(size: size, transition: .immediate) + } + } + return false + } + + @objc func textFieldTextChanged(_ sender: UITextField) { + if let color = self.colorFromCurrentText() { + self.setColor(color) + } + + if let (size, _) = self.validLayout { + self.updateSelectionLayout(size: size, transition: .immediate) + } + } + + @objc func textFieldShouldReturn(_ textField: UITextField) -> Bool { + self.skipEndEditing = true + if let color = self.colorFromCurrentText() { + self.setColor(color) + } else { + self.setColor(self.previousColor ?? .black, isDefault: self.previousIsDefault ?? false) + } + self.textFieldNode.textField.resignFirstResponder() + return false + } + + func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + self.skipEndEditing = false + self.previousColor = self.color + self.previousIsDefault = self.isDefault + + textField.textColor = self.theme.chat.inputPanel.inputTextColor + + return true + } + + @objc func textFieldDidEndEditing(_ textField: UITextField) { + if !self.skipEndEditing { + if let color = self.colorFromCurrentText() { + self.setColor(color) + } else { + self.setColor(self.previousColor ?? .black, isDefault: self.previousIsDefault ?? false) + } + } + } + + func setSkipEndEditingIfNeeded() { + if self.textFieldNode.textField.isFirstResponder && self.colorFromCurrentText() != nil { + self.skipEndEditing = true + } + } + + private func colorFromCurrentText() -> UIColor? { + if let text = self.textFieldNode.textField.text, text.count == 6, let color = UIColor(hexString: text) { + return color + } else { + return nil + } + } + + private func updateSelectionLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.measureNode.attributedText = NSAttributedString(string: self.textFieldNode.textField.text ?? "", font: self.textFieldNode.textField.font) + let size = self.measureNode.updateLayout(size) + transition.updateFrame(node: self.selectionNode, frame: CGRect(x: self.textFieldNode.frame.minX, y: 6.0, width: max(0.0, size.width), height: 20.0)) + } + + private func updateSelectionVisibility() { + self.selectionNode.isHidden = true + } + + func updateLayout(size: CGSize, condensed: Bool, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, condensed) + + let swatchFrame = CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 21.0, height: 21.0)) + transition.updateFrame(node: self.swatchNode, frame: swatchFrame) + transition.updateFrame(node: self.borderNode, frame: swatchFrame) + + self.swatchNode.isHidden = !self.displaySwatch + + let textPadding: CGFloat + if self.displaySwatch { + textPadding = condensed ? 31.0 : 37.0 + } else { + textPadding = 12.0 + } + + transition.updateFrame(node: self.textBackgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) + transition.updateFrame(node: self.textFieldNode, frame: CGRect(x: textPadding + 10.0, y: 1.0, width: size.width - (21.0 + textPadding), height: size.height - 2.0)) + + self.updateSelectionLayout(size: size, transition: transition) + + let prefixSize = self.prefixNode.measure(size) + transition.updateFrame(node: self.prefixNode, frame: CGRect(origin: CGPoint(x: textPadding - UIScreenPixel, y: 6.0), size: prefixSize)) + + let removeSize = CGSize(width: 30.0, height: 30.0) + let removeOffset: CGFloat = condensed ? 3.0 : 0.0 + transition.updateFrame(node: self.removeButton, frame: CGRect(origin: CGPoint(x: size.width - removeSize.width + removeOffset, y: 0.0), size: removeSize)) + self.removeButton.alpha = self.isRemovable ? 1.0 : 0.0 + } +} + +private final class ColorSampleItemNode: ASImageNode { + private struct State: Equatable { + var color: UInt32 + var size: CGSize + var isSelected: Bool + } + + private var action: () -> Void + private var validState: State? + + init(action: @escaping () -> Void) { + self.action = action + + super.init() + + self.isUserInteractionEnabled = true + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.action() + } + } + + func update(size: CGSize, color: UIColor, isSelected: Bool) { + let state = State(color: color.rgb, size: size, isSelected: isSelected) + if self.validState != state { + self.validState = state + + self.image = generateImage(CGSize(width: size.width, height: size.height), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setBlendMode(.softLight) + context.setStrokeColor(UIColor(white: 0.0, alpha: 0.3).cgColor) + context.setLineWidth(UIScreenPixel) + context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: UIScreenPixel, dy: UIScreenPixel)) + + if isSelected { + context.setBlendMode(.copy) + context.setStrokeColor(UIColor.clear.cgColor) + let lineWidth: CGFloat = 2.0 + context.setLineWidth(lineWidth) + let inset: CGFloat = 2.0 + lineWidth / 2.0 + context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: inset, dy: inset)) + } + }) + } + } +} diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 2100af4533..4a147aa594 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -1495,6 +1495,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } ) }, + contentIdUpdated: { _ in }, deviceMetrics: deviceMetrics, hiddenInputHeight: hiddenInputHeight, inputHeight: inputHeight, diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index 44fc6f1def..2b40665c5b 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -185,6 +185,7 @@ public final class EmojiStatusSelectionComponent: Component { switchToGifSubject: { _ in }, reorderItems: { _, _ in }, makeSearchContainerNode: { _ in return nil }, + contentIdUpdated: { _ in }, deviceMetrics: component.deviceMetrics, hiddenInputHeight: 0.0, inputHeight: 0.0, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 9f096552d8..41b0f421d2 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -2534,6 +2534,13 @@ public final class EmojiPagerContentComponent: Component { let frame: CGRect let supergroupId: AnyHashable let groupId: AnyHashable + let itemsPerRow: Int + let nativeItemSize: CGFloat + let visibleItemSize: CGFloat + let playbackItemSize: CGFloat + let horizontalSpacing: CGFloat + let verticalSpacing: CGFloat + let itemInsets: UIEdgeInsets let headerHeight: CGFloat let itemTopOffset: CGFloat let itemCount: Int @@ -2642,6 +2649,30 @@ public final class EmojiPagerContentComponent: Component { var verticalGroupOrigin: CGFloat = self.itemInsets.top self.itemGroupLayouts = [] for itemGroup in itemGroups { + var itemsPerRow = self.itemsPerRow + var nativeItemSize = self.nativeItemSize + var visibleItemSize = self.visibleItemSize + var playbackItemSize = self.playbackItemSize + var horizontalSpacing = self.horizontalSpacing + var verticalSpacing = self.verticalSpacing + var itemInsets = self.itemInsets + + if itemGroup.groupId == AnyHashable("stickers") { + let minItemsPerRow = 5 + nativeItemSize = 70.0 + playbackItemSize = 96.0 + verticalSpacing = 2.0 + let minSpacing = 12.0 + + itemInsets = UIEdgeInsets(top: containerInsets.top, left: containerInsets.left + 10.0, bottom: containerInsets.bottom, right: containerInsets.right + 10.0) + + let itemHorizontalSpace = width - itemInsets.left - itemInsets.right + itemsPerRow = max(minItemsPerRow, Int((itemHorizontalSpace + minSpacing) / (nativeItemSize + minSpacing))) + let proposedItemSize = floor((itemHorizontalSpace - minSpacing * (CGFloat(itemsPerRow) - 1.0)) / CGFloat(itemsPerRow)) + visibleItemSize = proposedItemSize < nativeItemSize ? proposedItemSize : nativeItemSize + horizontalSpacing = floorToScreenPixels((itemHorizontalSpace - visibleItemSize * CGFloat(itemsPerRow)) / CGFloat(itemsPerRow - 1)) + } + var itemTopOffset: CGFloat = 0.0 var headerHeight: CGFloat = 0.0 var groupSpacing = self.verticalGroupDefaultSpacing @@ -2663,7 +2694,7 @@ public final class EmojiPagerContentComponent: Component { if itemGroup.isEmbedded { numRowsInGroup = 0 } else { - numRowsInGroup = (itemGroup.itemCount + (self.itemsPerRow - 1)) / self.itemsPerRow + numRowsInGroup = (itemGroup.itemCount + (itemsPerRow - 1)) / itemsPerRow } var collapsedItemIndex: Int? @@ -2674,7 +2705,7 @@ public final class EmojiPagerContentComponent: Component { } else if let collapsedLineCount = itemGroup.collapsedLineCount, !expandedGroupIds.contains(itemGroup.groupId) { let maxLines: Int = collapsedLineCount if numRowsInGroup > maxLines { - visibleItemCount = self.itemsPerRow * maxLines - 1 + visibleItemCount = itemsPerRow * maxLines - 1 collapsedItemIndex = visibleItemCount collapsedItemText = "+\(itemGroup.itemCount - visibleItemCount)" } else { @@ -2685,10 +2716,10 @@ public final class EmojiPagerContentComponent: Component { } if !itemGroup.isEmbedded { - numRowsInGroup = (visibleItemCount + (self.itemsPerRow - 1)) / self.itemsPerRow + numRowsInGroup = (visibleItemCount + (itemsPerRow - 1)) / itemsPerRow } - var groupContentSize = CGSize(width: width, height: itemTopOffset + CGFloat(numRowsInGroup) * self.visibleItemSize + CGFloat(max(0, numRowsInGroup - 1)) * self.verticalSpacing) + var groupContentSize = CGSize(width: width, height: itemTopOffset + CGFloat(numRowsInGroup) * visibleItemSize + CGFloat(max(0, numRowsInGroup - 1)) * verticalSpacing) if (itemGroup.isPremiumLocked || itemGroup.isFeatured), case .compact = layoutType { groupContentSize.height += self.premiumButtonInset + self.premiumButtonHeight } @@ -2697,6 +2728,13 @@ public final class EmojiPagerContentComponent: Component { frame: CGRect(origin: CGPoint(x: 0.0, y: verticalGroupOrigin), size: groupContentSize), supergroupId: itemGroup.supergroupId, groupId: itemGroup.groupId, + itemsPerRow: itemsPerRow, + nativeItemSize: nativeItemSize, + visibleItemSize: visibleItemSize, + playbackItemSize: playbackItemSize, + horizontalSpacing: horizontalSpacing, + verticalSpacing: verticalSpacing, + itemInsets: itemInsets, headerHeight: headerHeight, itemTopOffset: itemTopOffset, itemCount: visibleItemCount, @@ -2705,24 +2743,24 @@ public final class EmojiPagerContentComponent: Component { )) verticalGroupOrigin += groupContentSize.height + groupSpacing } - verticalGroupOrigin += self.itemInsets.bottom + verticalGroupOrigin += itemInsets.bottom self.contentSize = CGSize(width: width, height: verticalGroupOrigin) } func frame(groupIndex: Int, itemIndex: Int) -> CGRect { let groupLayout = self.itemGroupLayouts[groupIndex] - let row = itemIndex / self.itemsPerRow - let column = itemIndex % self.itemsPerRow + let row = itemIndex / groupLayout.itemsPerRow + let column = itemIndex % groupLayout.itemsPerRow return CGRect( origin: CGPoint( - x: self.itemInsets.left + CGFloat(column) * (self.visibleItemSize + self.horizontalSpacing), - y: groupLayout.frame.minY + groupLayout.itemTopOffset + CGFloat(row) * (self.visibleItemSize + self.verticalSpacing) + x: groupLayout.itemInsets.left + CGFloat(column) * (groupLayout.visibleItemSize + groupLayout.horizontalSpacing), + y: groupLayout.frame.minY + groupLayout.itemTopOffset + CGFloat(row) * (groupLayout.visibleItemSize + groupLayout.verticalSpacing) ), size: CGSize( - width: self.visibleItemSize, - height: self.visibleItemSize + width: groupLayout.visibleItemSize, + height: groupLayout.visibleItemSize ) ) } @@ -2731,22 +2769,22 @@ public final class EmojiPagerContentComponent: Component { var result: [(supergroupId: AnyHashable, groupId: AnyHashable, groupIndex: Int, groupItems: Range?)] = [] for groupIndex in 0 ..< self.itemGroupLayouts.count { - let group = self.itemGroupLayouts[groupIndex] + let groupLayout = self.itemGroupLayouts[groupIndex] - if !rect.intersects(group.frame) { + if !rect.intersects(groupLayout.frame) { continue } - let offsetRect = rect.offsetBy(dx: -self.itemInsets.left, dy: -group.frame.minY - group.itemTopOffset) - var minVisibleRow = Int(floor((offsetRect.minY - self.verticalSpacing) / (self.visibleItemSize + self.verticalSpacing))) + let offsetRect = rect.offsetBy(dx: -groupLayout.itemInsets.left, dy: -groupLayout.frame.minY - groupLayout.itemTopOffset) + var minVisibleRow = Int(floor((offsetRect.minY - groupLayout.verticalSpacing) / (groupLayout.visibleItemSize + groupLayout.verticalSpacing))) minVisibleRow = max(0, minVisibleRow) - let maxVisibleRow = Int(ceil((offsetRect.maxY - self.verticalSpacing) / (self.visibleItemSize + self.verticalSpacing))) + let maxVisibleRow = Int(ceil((offsetRect.maxY - groupLayout.verticalSpacing) / (groupLayout.visibleItemSize + groupLayout.verticalSpacing))) - let minVisibleIndex = minVisibleRow * self.itemsPerRow - let maxVisibleIndex = min(group.itemCount - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1) + let minVisibleIndex = minVisibleRow * groupLayout.itemsPerRow + let maxVisibleIndex = min(groupLayout.itemCount - 1, (maxVisibleRow + 1) * groupLayout.itemsPerRow - 1) result.append(( - supergroupId: group.supergroupId, - groupId: group.groupId, + supergroupId: groupLayout.supergroupId, + groupId: groupLayout.groupId, groupIndex: groupIndex, groupItems: maxVisibleIndex >= minVisibleIndex ? (minVisibleIndex ..< (maxVisibleIndex + 1)) : nil )) @@ -5204,6 +5242,7 @@ public final class EmojiPagerContentComponent: Component { if !itemGroup.isEmbedded, let groupItemRange = groupItems.groupItems { for index in groupItemRange.lowerBound ..< groupItemRange.upperBound { let item = itemGroup.items[index] + if assignTopVisibleSubgroupId { if let subgroupId = item.subgroupId { @@ -5219,9 +5258,9 @@ public final class EmojiPagerContentComponent: Component { let itemDimensions: CGSize = item.animationData?.dimensions ?? CGSize(width: 512.0, height: 512.0) - let itemNativeFitSize = itemDimensions.aspectFitted(CGSize(width: itemLayout.nativeItemSize, height: itemLayout.nativeItemSize)) - let itemVisibleFitSize = itemDimensions.aspectFitted(CGSize(width: itemLayout.visibleItemSize, height: itemLayout.visibleItemSize)) - let itemPlaybackSize = itemDimensions.aspectFitted(CGSize(width: itemLayout.playbackItemSize, height: itemLayout.playbackItemSize)) + let itemNativeFitSize = itemDimensions.aspectFitted(CGSize(width: itemGroupLayout.nativeItemSize, height: itemGroupLayout.nativeItemSize)) + let itemVisibleFitSize = itemDimensions.aspectFitted(CGSize(width: itemGroupLayout.visibleItemSize, height: itemGroupLayout.visibleItemSize)) + let itemPlaybackSize = itemDimensions.aspectFitted(CGSize(width: itemGroupLayout.playbackItemSize, height: itemGroupLayout.playbackItemSize)) var animateItemIn = false var updateItemLayerPlaceholder = false @@ -6309,6 +6348,8 @@ public final class EmojiPagerContentComponent: Component { isEmojiSelection: Bool, isTopicIconSelection: Bool = false, isQuickReactionSelection: Bool = false, + isProfilePhotoEmojiSelection: Bool = false, + isGroupPhotoEmojiSelection: Bool = false, topReactionItems: [EmojiComponentReactionItem], areUnicodeEmojiEnabled: Bool, areCustomEmojiEnabled: Bool, @@ -6359,6 +6400,8 @@ public final class EmojiPagerContentComponent: Component { } } |> take(1) + } else if isProfilePhotoEmojiSelection { + //orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedProfilePhotoEmoji) } let availableReactions: Signal @@ -7071,7 +7114,7 @@ public final class EmojiPagerContentComponent: Component { } var displaySearchWithPlaceholder: String? - var searchInitiallyHidden = true + let searchInitiallyHidden = true if hasSearch { if isReactionSelection { displaySearchWithPlaceholder = strings.EmojiSearch_SearchReactionsPlaceholder @@ -7079,7 +7122,8 @@ public final class EmojiPagerContentComponent: Component { displaySearchWithPlaceholder = strings.EmojiSearch_SearchStatusesPlaceholder } else if isEmojiSelection { displaySearchWithPlaceholder = strings.EmojiSearch_SearchEmojiPlaceholder - searchInitiallyHidden = false + } else if isProfilePhotoEmojiSelection || isGroupPhotoEmojiSelection { + displaySearchWithPlaceholder = "Search" } } @@ -7147,7 +7191,8 @@ public final class EmojiPagerContentComponent: Component { chatPeerId: EnginePeer.Id?, hasSearch: Bool, hasTrending: Bool, - forceHasPremium: Bool + forceHasPremium: Bool, + searchIsPlaceholderOnly: Bool = true ) -> Signal { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let isPremiumDisabled = premiumConfiguration.isPremiumDisabled @@ -7637,7 +7682,7 @@ public final class EmojiPagerContentComponent: Component { warpContentsOnEdges: false, displaySearchWithPlaceholder: hasSearch ? strings.StickersSearch_SearchStickersPlaceholder : nil, searchInitiallyHidden: true, - searchIsPlaceholderOnly: true, + searchIsPlaceholderOnly: searchIsPlaceholderOnly, emptySearchResults: nil, enableLongPress: false, selectedItems: Set() diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index ce4688b83d..1b9356f34a 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -108,6 +108,7 @@ public final class EntityKeyboardComponent: Component { public let switchToGifSubject: (GifPagerContentComponent.Subject) -> Void public let reorderItems: (ReorderCategory, [EntityKeyboardTopPanelComponent.Item]) -> Void public let makeSearchContainerNode: (EntitySearchContentType) -> EntitySearchContainerNode? + public let contentIdUpdated: (AnyHashable) -> Void public let deviceMetrics: DeviceMetrics public let hiddenInputHeight: CGFloat public let inputHeight: CGFloat @@ -138,6 +139,7 @@ public final class EntityKeyboardComponent: Component { switchToGifSubject: @escaping (GifPagerContentComponent.Subject) -> Void, reorderItems: @escaping (ReorderCategory, [EntityKeyboardTopPanelComponent.Item]) -> Void, makeSearchContainerNode: @escaping (EntitySearchContentType) -> EntitySearchContainerNode?, + contentIdUpdated: @escaping (AnyHashable) -> Void, deviceMetrics: DeviceMetrics, hiddenInputHeight: CGFloat, inputHeight: CGFloat, @@ -167,6 +169,7 @@ public final class EntityKeyboardComponent: Component { self.switchToGifSubject = switchToGifSubject self.reorderItems = reorderItems self.makeSearchContainerNode = makeSearchContainerNode + self.contentIdUpdated = contentIdUpdated self.deviceMetrics = deviceMetrics self.hiddenInputHeight = hiddenInputHeight self.inputHeight = inputHeight @@ -744,6 +747,12 @@ public final class EntityKeyboardComponent: Component { } strongSelf.isTopPanelHiddenUpdated(isTopPanelHidden: isTopPanelHidden, transition: transition) }, + contentIdUpdated: { [weak self] id in + guard let strongSelf = self, let component = strongSelf.component else { + return + } + component.contentIdUpdated(id) + }, panelHideBehavior: panelHideBehavior, clipContentToTopPanel: component.clipContentToTopPanel )), @@ -862,6 +871,15 @@ public final class EntityKeyboardComponent: Component { component.hideTopPanelUpdated(self.isTopPanelHidden, transition) } + public func scrollToContentId(_ contentId: AnyHashable) { + guard let _ = self.component else { + return + } + if let pagerView = self.pagerView.findTaggedView(tag: PagerComponentViewTag()) as? PagerComponent.View { + pagerView.navigateToContentId(contentId) + } + } + public func openSearch() { guard let component = self.component else { return diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift index 5cf399cfda..50390d204e 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift @@ -416,6 +416,7 @@ private final class TopicIconSelectionComponent: Component { switchToGifSubject: { _ in }, reorderItems: { _, _ in }, makeSearchContainerNode: { _ in return nil }, + contentIdUpdated: { _ in }, deviceMetrics: component.deviceMetrics, hiddenInputHeight: 0.0, inputHeight: 0.0, diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 5c3b4630fa..3bf5f3cc92 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -84,6 +84,7 @@ import StickerPackPreviewUI import ChatListHeaderComponent import ChatControllerInteraction import StorageUsageScreen +import AvatarEditorScreen enum PeerInfoAvatarEditingMode { case generic @@ -7208,6 +7209,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate completion(nil) } } + mixin.requestAvatarEditor = { [weak self] completion in + guard let strongSelf = self, let completion else { + return + } + + let controller = AvatarEditorScreen(context: strongSelf.context) + controller.completion = completion + (strongSelf.controller?.navigationController?.topViewController as? ViewController)?.push(controller) + } + if let confirmationTextPhoto, let confirmationAction { mixin.willFinishWithImage = { [weak self] image, commit in if let strongSelf = self, let image { diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index b7a912a8ad..080097b6e4 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -15,7 +15,6 @@ import AppBundle import DatePickerNode import DebugSettingsUI import TabBarUI -import DrawingUI public final class TelegramRootController: NavigationController { private let context: AccountContext diff --git a/submodules/WebSearchUI/Sources/LegacyWebSearchEditor.swift b/submodules/WebSearchUI/Sources/LegacyWebSearchEditor.swift index 7032cf6f5e..15c406f9f9 100644 --- a/submodules/WebSearchUI/Sources/LegacyWebSearchEditor.swift +++ b/submodules/WebSearchUI/Sources/LegacyWebSearchEditor.swift @@ -8,6 +8,7 @@ import Display import TelegramPresentationData import AccountContext import LegacyUI +import LegacyMediaPickerUI func presentLegacyWebSearchEditor(context: AccountContext, theme: PresentationTheme, result: ChatContextResult, initialLayout: ContainerViewLayout?, updateHiddenMedia: @escaping (String?) -> Void, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (ChatContextResult) -> UIView?, completed: @escaping (UIImage) -> Void, present: @escaping (ViewController, Any?) -> Void) { guard let item = legacyWebSearchItem(account: context.account, result: result) else { @@ -33,7 +34,10 @@ func presentLegacyWebSearchEditor(context: AccountContext, theme: PresentationTh let legacyController = LegacyController(presentation: .custom, theme: theme, initialLayout: initialLayout) legacyController.statusBar.statusBarStyle = theme.rootController.statusBarStyle.style + let paintStickersContext = LegacyPaintStickersContext(context: context) + let controller = TGPhotoEditorController(context: legacyController.context, item: item, intent: TGPhotoEditorControllerAvatarIntent, adjustments: nil, caption: nil, screenImage: screenImage ?? UIImage(), availableTabs: TGPhotoEditorController.defaultTabsForAvatarIntent(), selectedTab: .cropTab)! + controller.stickersContext = paintStickersContext legacyController.bind(controller: controller) controller.editingContext = TGMediaEditingContext()