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 TelegramStringFormatting import TelegramNotices import EntityKeyboard import PagerComponent import Markdown import GradientBackground import LegacyComponents import DrawingUI import ButtonComponent import AnimationCache import EmojiTextAttachmentView import MediaEditor import AvatarBackground import LottieComponent import UndoUI import PremiumAlertController import GlassBarButtonComponent import BundleIconComponent public struct AvatarKeyboardInputData: 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 let peerType: AvatarEditorScreen.PeerType let markup: TelegramMediaImage.EmojiMarkup? init( context: AccountContext, ready: Promise, peerType: AvatarEditorScreen.PeerType, markup: TelegramMediaImage.EmojiMarkup? ) { self.context = context self.ready = ready self.peerType = peerType self.markup = markup } static func ==(lhs: AvatarEditorScreenComponent, rhs: AvatarEditorScreenComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.peerType != rhs.peerType { return false } if lhs.markup != rhs.markup { return false } return true } final class State: ComponentState { let context: AccountContext let ready: Promise var selectedBackground: AvatarBackground var selectedFile: TelegramMediaFile? var keyboardContentId: AnyHashable = "emoji" var expanded: Bool = false var editingColor: Bool = false var previousColor: AvatarBackground var previousCustomColor: AvatarBackground? var customColor: AvatarBackground? var isSearchActive: Bool = false private var fileDisposable: Disposable? init(context: AccountContext, ready: Promise, markup: TelegramMediaImage.EmojiMarkup?) { self.context = context self.ready = ready self.selectedBackground = AvatarBackground.defaultBackgrounds.first! self.previousColor = self.selectedBackground super.init() if let markup { switch markup.content { case let .emoji(fileId): self.fileDisposable = (context.engine.stickers.resolveInlineStickers(fileIds: [fileId]) |> deliverOnMainQueue).start(next: { [weak self] files in if let strongSelf = self, let file = files.values.first { strongSelf.selectedFile = file strongSelf.updated(transition: .immediate) } }) case let .sticker(packReference, fileId): self.fileDisposable = (context.engine.stickers.loadedStickerPack(reference: packReference, forceActualized: false) |> map { pack -> TelegramMediaFile? in if case let .result(_, items, _) = pack, let item = items.first(where: { $0.file.fileId.id == fileId }) { return item.file._parse() } return nil } |> deliverOnMainQueue).start(next: { [weak self] file in if let strongSelf = self, let file { strongSelf.selectedFile = file strongSelf.updated(transition: .immediate) } }) } var isPremium = false let colorsValue = markup.backgroundColors.map { UInt32(bitPattern: $0) } if let defaultColor = AvatarBackground.defaultBackgrounds.first(where: { $0.colors == colorsValue}) { if defaultColor.isPremium { isPremium = true } } self.selectedBackground = .gradient(colorsValue, isPremium) self.previousColor = self.selectedBackground } else { self.selectedBackground = AvatarBackground.defaultBackgrounds.first! } self.previousColor = self.selectedBackground } deinit { self.fileDisposable?.dispose() } } func makeState() -> State { return State( context: self.context, ready: self.ready, markup: self.markup ) } private struct EmojiSearchResult { var groups: [EmojiPagerContentComponent.ItemGroup] var id: AnyHashable var version: Int var isPreset: Bool var canLoadMore: Bool } private struct EmojiSearchState { var result: EmojiSearchResult? var isSearching: Bool init(result: EmojiSearchResult?, isSearching: Bool) { self.result = result self.isSearching = isSearching } } class View: UIView { 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 var environment: EnvironmentType? private weak var state: State? private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)? private var controller: (() -> AvatarEditorScreen?)? private var dataDisposable: Disposable? private var data: AvatarKeyboardInputData? private let emojiSearchDisposable = MetaDisposable() private var stickerSearchContext: StickerSearchContext? private let emojiSearchState = Promise(EmojiSearchState(result: nil, isSearching: false)) private var emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) { didSet { self.emojiSearchState.set(.single(self.emojiSearchStateValue)) } } 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 = 26.0 self.keyboardContainerView = UIView() self.keyboardContainerView.clipsToBounds = true self.keyboardContainerView.layer.cornerRadius = 26.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: AvatarKeyboardInputData) { let wasEmpty = self.data == nil self.data = data if wasEmpty && self.state?.selectedFile == nil { self.state?.selectedFile = data.emoji.panelItemGroups.first?.items.first?.itemFile?._parse() } let updateSearchQuery: (EmojiPagerContentComponent.SearchQuery?) -> Void = { [weak self] query in guard let self, let context = self.state?.context else { return } switch query { case .none: self.stickerSearchContext = nil self.emojiSearchDisposable.set(nil) self.emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) case let .text(rawQuery, languageCode): let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) if query.isEmpty { self.stickerSearchContext = nil self.emojiSearchDisposable.set(nil) self.emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) } else { self.stickerSearchContext = nil let emojiItemsSignal = context.engine.itemCollections.allItems(namespace: Namespaces.ItemCollection.CloudEmojiPacks) |> take(1) let buildGroups: ([EngineRawItemCollectionItem], [String: String], StickerSearchContext.State, StickerSearchContext?) -> (groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: StickerSearchContext?) = { items, allEmoticons, stickerState, searchContext in let hasPremium = true var emoji: [(String, TelegramMediaFile.Accessor?, String)] = [] for rawItem in items { guard let item = rawItem as? StickerPackItem else { continue } if let alt = item.file.customEmojiAlt { if !item.file.isPremiumEmoji || hasPremium { if !alt.isEmpty, let keyword = allEmoticons[alt] { emoji.append((alt, item.file, keyword)) } else if alt == query { emoji.append((alt, item.file, alt)) } } } } var emojiItems: [EmojiPagerContentComponent.Item] = [] var existingIds = Set() for item in emoji { 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 sticker in stickerState.items { if existingIds.contains(sticker.file.fileId) { continue } existingIds.insert(sticker.file.fileId) let animationData = EntityKeyboardAnimationData(file: TelegramMediaFile.Accessor(sticker.file)) let item = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: TelegramMediaFile.Accessor(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, badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, isEmbedded: false, hasClear: false, hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, fillWithLoadingPlaceholders: false, items: emojiItems ) ) } if !stickerItems.isEmpty { result.append( EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "stickers", title: "Stickers", subtitle: nil, badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, isEmbedded: false, hasClear: false, hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, fillWithLoadingPlaceholders: false, items: stickerItems ) ) } return (result, stickerState.canLoadMore, stickerState.items.isEmpty && stickerState.isLoadingMore, searchContext) } let resultSignal: Signal<(groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: StickerSearchContext?), NoError> if query.isSingleEmoji { let allEmoticons = [query.basicEmoji.0: query.basicEmoji.0] let searchContext = context.engine.stickers.stickerSearchContext(query: nil, emoticon: [query.basicEmoji.0], inputLanguageCode: languageCode) resultSignal = combineLatest(emojiItemsSignal, searchContext.state) |> map { items, stickerState in return buildGroups(items, allEmoticons, stickerState, searchContext) } } else { var keywordsSignal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) if !languageCode.lowercased().hasPrefix("en") { keywordsSignal = keywordsSignal |> 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 } ) } } resultSignal = keywordsSignal |> mapToSignal { keywords -> Signal<(groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: StickerSearchContext?), NoError> in var allEmoticons: [String: String] = [:] for keyword in keywords { for emoticon in keyword.emoticons { allEmoticons[emoticon] = keyword.keyword } } let emoticon = Array(allEmoticons.keys) guard !emoticon.isEmpty else { return combineLatest(emojiItemsSignal, .single(StickerSearchContext.State(items: [], canLoadMore: false, isLoadingMore: false))) |> map { items, stickerState in return buildGroups(items, allEmoticons, stickerState, nil) } } let searchContext = context.engine.stickers.stickerSearchContext(query: query, emoticon: emoticon, inputLanguageCode: languageCode) return combineLatest(emojiItemsSignal, searchContext.state) |> map { items, stickerState in return buildGroups(items, allEmoticons, stickerState, searchContext) } } } var version = 0 self.emojiSearchStateValue.isSearching = true self.emojiSearchDisposable.set((resultSignal |> delay(0.15, queue: .mainQueue()) |> deliverOnMainQueue).start(next: { [weak self] result in guard let self else { return } self.stickerSearchContext = result.searchContext self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.groups, id: AnyHashable(query), version: version, isPreset: false, canLoadMore: result.canLoadMore), isSearching: result.isSearching) version += 1 })) } case let .category(value): self.stickerSearchContext = nil let resultSignal = context.engine.stickers.searchEmoji(category: value) |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] var existingIds = Set() for itemFile in files { if existingIds.contains(itemFile.fileId) { continue } existingIds.insert(itemFile.fileId) let animationData = EntityKeyboardAnimationData(file: TelegramMediaFile.Accessor(itemFile)) let item = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: TelegramMediaFile.Accessor(itemFile), subgroupId: nil, icon: .none, tintMode: animationData.isTemplate ? .primary : .none ) items.append(item) } return .single(([EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "search", title: nil, subtitle: nil, badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, isEmbedded: false, hasClear: false, hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, fillWithLoadingPlaceholders: false, items: items )], isFinalResult)) } let _ = resultSignal var version = 0 self.emojiSearchDisposable.set((resultSignal |> deliverOnMainQueue).start(next: { [weak self] result in guard let self else { return } guard let group = result.items.first else { return } if group.items.isEmpty && !result.isFinalResult { //self.emojiSearchStateValue.isSearching = true self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: [ EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "search", title: nil, subtitle: nil, badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, isEmbedded: false, hasClear: false, hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, fillWithLoadingPlaceholders: true, items: [] ) ], id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) return } self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) version += 1 })) } } data.emoji.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak self] _, item, _, _, _, _ in guard let self, let _ = item.itemFile else { return } self.state?.selectedFile = item.itemFile?._parse() 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? EngineItemCollectionId else { return } let context = controller.context let _ = (context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks)) |> take(1) |> deliverOnMainQueue).start(next: { items in for featuredStickerPack in 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._parse(), items: items) |> map { _ in return Void() } } 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 let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 } if groupId == AnyHashable("recent") { let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize)) var items: [ActionSheetItem] = [] items.append(ActionSheetButtonItem(title: presentationData.strings.Emoji_ClearRecent, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let _ = context.engine.stickers.clearRecentlyUsedEmoji().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("popular") { let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize)) var items: [ActionSheetItem] = [] 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) } }, editAction: { _ in }, 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: { query in updateSearchQuery(query) }, updateScrollingToItemGroup: { }, onScroll: { [weak self] in if let self { self.endEditing(true) if let state = self.state, state.expanded { state.expanded = false state.updated(transition: ComponentTransition(animation: .curve(duration: 0.45, curve: .spring))) } } }, loadMore: { [weak self] in self?.stickerSearchContext?.loadMore() }, chatPeerId: nil, peekBehavior: nil, customLayout: nil, externalBackground: nil, externalExpansionView: nil, customContentView: nil, useOpaqueTheme: true, hideBackground: true, stateContext: nil, addImage: nil ) data.stickers?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak self] _, item, _, _, _, _ in guard let self, let _ = item.itemFile else { return } self.state?.selectedFile = item.itemFile?._parse() 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? EngineItemCollectionId else { return } let context = controller.context let _ = (context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)) |> take(1) |> deliverOnMainQueue).start(next: { items in for featuredStickerPack in 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._parse(), items: items) |> map { _ in return Void() } } 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 } 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 _ = (context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)) |> take(1) |> deliverOnMainQueue).start(next: { items in var stickerPackIds: [Int64] = [] for featuredStickerPack in 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") { } }, editAction: { _ in }, 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: { query in updateSearchQuery(query) }, updateScrollingToItemGroup: { }, onScroll: { [weak self] in if let self { self.endEditing(true) if let state = self.state, state.expanded { state.expanded = false state.updated(transition: ComponentTransition(animation: .curve(duration: 0.45, curve: .spring))) } } }, loadMore: { [weak self] in self?.stickerSearchContext?.loadMore() }, chatPeerId: nil, peekBehavior: nil, customLayout: nil, externalBackground: nil, externalExpansionView: nil, customContentView: nil, useOpaqueTheme: true, hideBackground: true, stateContext: nil, addImage: nil ) self.state?.updated(transition: .immediate) self.state?.ready.set(.single(true)) } private var isExpanded = false func update(component: AvatarEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let environment = environment[EnvironmentType.self].value self.environment = environment self.state = state let strings = environment.strings let theme = environment.theme let controller = environment.controller self.controller = { return controller() as? AvatarEditorScreen } self.navigationMetrics = (environment.navigationHeight, environment.statusBarHeight) let sideInset: CGFloat = 16.0 + environment.safeInsets.left if state.expanded && environment.inputHeight > 0.0 { state.expanded = false } 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) } if let navigationDoneButton = self.navigationDoneButton.view, !navigationDoneButton.alpha.isZero, let snapshotView = self.navigationDoneButton.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 backgroundIsBright = UIColor(rgb: state.selectedBackground.colors.first ?? 0).lightness > 0.8 //state.expanded && !backgroundIsBright ? .white : environment.theme.rootController.navigationBar.accentTextColor let navigationCancelButtonSize = self.navigationCancelButton.update( transition: transition, component: AnyComponent( GlassBarButtonComponent( size: CGSize(width: 44.0, height: 44.0), backgroundColor: nil, isDark: environment.theme.overallDarkAppearance, state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent(BundleIconComponent(name: "Navigation/Close", tintColor: environment.theme.chat.inputPanel.panelControlColor))), action: { [weak self] _ in guard let self else { return } self.controller?()?.dismiss() } ) ), environment: {}, containerSize: CGSize(width: 44.0, height: 44.0) ) 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: 16.0), size: navigationCancelButtonSize)) transition.setAlpha(view: navigationCancelButtonView, alpha: !state.editingColor ? 1.0 : 0.0) } let navigationDoneButtonSize = self.navigationDoneButton.update( transition: transition, component: AnyComponent( GlassBarButtonComponent( size: CGSize(width: 44.0, height: 44.0), backgroundColor: environment.theme.list.itemCheckColors.fillColor, isDark: environment.theme.overallDarkAppearance, state: .tintedGlass, component: AnyComponentWithIdentity(id: "done", component: AnyComponent(BundleIconComponent(name: "Navigation/Done", tintColor: environment.theme.list.itemCheckColors.foregroundColor))), action: { [weak self] _ in guard let self else { return } self.complete() } ) ), environment: {}, containerSize: CGSize(width: 44.0, height: 44.0) ) 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: 16.0), size: navigationDoneButtonSize)) transition.setAlpha(view: navigationDoneButtonView, alpha: (state.expanded || environment.inputHeight > 0.0) && !state.editingColor ? 1.0 : 0.0) } self.backgroundColor = environment.theme.list.blocksBackgroundColor self.backgroundContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor self.keyboardContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor self.panelSeparatorView.backgroundColor = environment.theme.list.itemPlainSeparatorColor if self.dataDisposable == nil, let controller = controller() as? AvatarEditorScreen { let context = component.context let signal = combineLatest(queue: .mainQueue(), controller.inputData |> delay(0.01, queue: .mainQueue()), self.emojiSearchState.get() ) self.dataDisposable = (signal |> deliverOnMainQueue ).start(next: { [weak self, weak state] data, emojiSearchState in if let self { var data = data if let searchResult = emojiSearchState.result { let presentationData = context.sharedContext.currentPresentationData.with { $0 } var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? if !emojiSearchState.isSearching && !searchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) { emptySearchResults = EmojiPagerContentComponent.EmptySearchResults( text: presentationData.strings.EmojiSearch_SearchEmojiEmptyResult, iconFile: nil ) } let searchState: EmojiPagerContentComponent.SearchState = emojiSearchState.isSearching ? .searching : .active if state?.keyboardContentId == AnyHashable("emoji") { data.emoji = data.emoji.withUpdatedItemGroups(panelItemGroups: data.emoji.panelItemGroups, contentItemGroups: searchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: searchResult.id, version: searchResult.version), emptySearchResults: emptySearchResults, searchState: searchState, canLoadMore: searchResult.canLoadMore) } else { data.stickers = data.stickers?.withUpdatedItemGroups(panelItemGroups: data.stickers?.panelItemGroups ?? searchResult.groups, contentItemGroups: searchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: searchResult.id, version: searchResult.version), emptySearchResults: emptySearchResults, searchState: searchState, canLoadMore: searchResult.canLoadMore) } } else if emojiSearchState.isSearching { if state?.keyboardContentId == AnyHashable("emoji") { data.emoji = data.emoji.withUpdatedItemGroups(panelItemGroups: data.emoji.panelItemGroups, contentItemGroups: data.emoji.contentItemGroups, itemContentUniqueId: data.emoji.itemContentUniqueId, emptySearchResults: data.emoji.emptySearchResults, searchState: .searching) } else { data.stickers = data.stickers?.withUpdatedItemGroups(panelItemGroups: data.stickers?.panelItemGroups ?? [], contentItemGroups: data.stickers?.contentItemGroups ?? [], itemContentUniqueId: data.stickers?.itemContentUniqueId, emptySearchResults: data.stickers?.emptySearchResults, searchState: .searching) } } 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.selectedFile, tapped: { [weak state, weak self] in if let state, !state.editingColor { if let emojiView = self?.keyboardView.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View { emojiView.ensureSearchUnfocused() } else if let emojiView = self?.keyboardView.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("stickers"))) as? EmojiPagerContentComponent.View { emojiView.ensureSearchUnfocused() } state.expanded = !state.expanded state.updated(transition: ComponentTransition(animation: .curve(duration: 0.35, curve: .spring))) } } ) ), 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 / (component.peerType == .forum ? 4.0 : 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: strings.AvatarEditor_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, isPremium: component.context.isPremium, values: AvatarBackground.defaultBackgrounds, selectedValue: state.selectedBackground, customValue: state.customColor, updateValue: { [weak state] value in if let state { state.selectedBackground = value state.updated(transition: .easeInOut(duration: 0.2)) } }, openColorPicker: { [weak self, weak state] in if let self, let state { self.endEditing(true) state.editingColor = true state.previousColor = state.selectedBackground state.previousCustomColor = state.customColor 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) } var colorPickerBottomInset: CGFloat = 0.0 if environment.deviceMetrics.type != .tablet { colorPickerBottomInset = environment.safeInsets.bottom } let colorPickerSize = self.colorPickerView.update( transition: transition, component: AnyComponent( ColorPickerComponent( theme: environment.theme, strings: environment.strings, isVisible: state.editingColor, bottomInset: colorPickerBottomInset, colors: state.selectedBackground.colors, colorsChanged: { [weak state] colors in if let state { state.customColor = .gradient(colors, true) state.selectedBackground = .gradient(colors, true) state.updated(transition: .immediate) } }, cancel: { [weak state] in if let state { state.selectedBackground = state.previousColor state.customColor = state.previousCustomColor state.editingColor = false state.updated(transition: .easeInOut(duration: 0.3)) } }, done: { [weak state] in if let state { state.editingColor = false state.customColor = state.selectedBackground state.updated(transition: .easeInOut(duration: 0.3)) } } ) ), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) ) 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 = strings.AvatarEditor_EmojiOrSticker keyboardSwitchTitle = " " } else if state.keyboardContentId == AnyHashable("emoji") { keyboardTitle = strings.AvatarEditor_Emoji keyboardSwitchTitle = strings.AvatarEditor_SwitchToStickers } else if state.keyboardContentId == AnyHashable("stickers") { keyboardTitle = strings.AvatarEditor_Stickers keyboardSwitchTitle = strings.AvatarEditor_SwitchToEmoji } else { keyboardTitle = " " keyboardSwitchTitle = " " } let keyboardTitleSize = self.keyboardTitleView.update( transition: .immediate, 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) } keyboardTitleView.bounds = CGRect(origin: .zero, size: keyboardTitleFrame.size) if keyboardTitleFrame.center.y == keyboardTitleView.center.y { keyboardTitleView.center = keyboardTitleFrame.center } else { transition.setPosition(view: keyboardTitleView, position: keyboardTitleFrame.center) } transition.setAlpha(view: keyboardTitleView, alpha: state.editingColor ? 0.0 : 1.0) } let keyboardSwitchSize = self.keyboardSwitchView.update( transition: .immediate, 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) } keyboardSwitchView.bounds = CGRect(origin: .zero, size: keyboardSwitchFrame.size) if keyboardSwitchFrame.center.y == keyboardSwitchView.center.y { keyboardSwitchView.center = keyboardSwitchFrame.center } else { transition.setPosition(view: keyboardSwitchView, position: keyboardSwitchFrame.center) } transition.setAlpha(view: keyboardSwitchView, alpha: state.editingColor ? 0.0 : 1.0) } contentHeight += keyboardTitleSize.height contentHeight += 8.0 let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) var bottomInset: CGFloat = buttonInsets.bottom if !effectiveIsExpanded { bottomInset += 52.0 + buttonInsets.bottom - 14.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, externalTintMaskContainer: nil, displayTopPanelBackground: .blur, topPanelExtensionUpdated: { _, _ in }, topPanelScrollingOffset: { _, _ in }, hideInputUpdated: { _, _, _ in }, hideTopPanelUpdated: { [weak self] hideTopPanel, transition in if let strongSelf = self { strongSelf.state?.isSearchActive = hideTopPanel if hideTopPanel { strongSelf.state?.expanded = false } 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, useExternalSearchContainer: 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 buttonText: String switch component.peerType { case .suggest: buttonText = strings.AvatarEditor_SuggestProfilePhoto case .user: buttonText = strings.AvatarEditor_SetProfilePhoto case .group, .forum: buttonText = strings.AvatarEditor_SetGroupPhoto case .channel: buttonText = strings.AvatarEditor_SetChannelPhoto } var isLocked = false if component.peerType != .suggest, !component.context.isPremium { if state.selectedBackground.isPremium { isLocked = true } if let selectedFile = state.selectedFile { if selectedFile.isSticker { isLocked = true } } } var buttonContents: [AnyComponentWithIdentity] = [] buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(buttonText), component: AnyComponent( Text(text: buttonText, font: Font.semibold(17.0), color: theme.list.itemCheckColors.foregroundColor) ))) if !component.context.isPremium && isLocked { buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(LottieComponent( content: LottieComponent.AppBundleContent(name: "premium_unlock"), color: theme.list.itemCheckColors.foregroundColor, startingPosition: .begin, size: CGSize(width: 30.0, height: 30.0), loop: true )))) } let buttonSize = self.buttonView.update( transition: transition, component: AnyComponent( ButtonComponent( background: ButtonComponent.Background( style: .glass, color: theme.list.itemCheckColors.fillColor, foreground: theme.list.itemCheckColors.foregroundColor, pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) ), content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( HStack(buttonContents, spacing: 3.0) )), isEnabled: true, displaysProgress: false, action: { [weak self] in self?.complete() } ) ), environment: {}, containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0) ) if let buttonView = self.buttonView.view { if buttonView.superview == nil { self.addSubview(buttonView) } transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: buttonSize)) } let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight - 4.0), size: CGSize(width: availableSize.width, height: availableSize.height - contentHeight + 4.0)) if let controller = environment.controller(), !controller.automaticallyControlPresentationContextLayout { let layout = ContainerViewLayout( size: availableSize, metrics: environment.metrics, deviceMetrics: environment.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomPanelFrame.height, right: 0.0), safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right), additionalInsets: .zero, statusBarHeight: environment.statusBarHeight, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false ) controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition) } return availableSize } private func presentPremiumToast() { guard let environment = self.environment, let component = self.component, let state = self.state, let parentController = environment.controller() else { return } HapticFeedback().impact(.light) var text: String = environment.strings.AvatarEditor_PremiumNeeded_Background if let selectedFile = state.selectedFile { if selectedFile.isSticker { text = environment.strings.AvatarEditor_PremiumNeeded_Sticker } } let controller = premiumAlertController( context: component.context, parentController: parentController, text: text ) parentController.present(controller, in: .window(.root)) } private func isPremiumRequired() -> Bool { guard let component = self.component, let state = self.state else { return false } if component.peerType != .suggest, !component.context.isPremium { if state.selectedBackground.isPremium { return true } if let selectedFile = state.selectedFile { if selectedFile.isSticker { return true } } } return false } private let queue = Queue() func complete() { guard let state = self.state, let file = state.selectedFile, let controller = self.controller?() else { return } if self.isPremiumRequired() { self.presentPremiumToast() return } let context = controller.context let _ = context.animationCache.getFirstFrame(queue: self.queue, sourceId: file.resource.id.stringRepresentation, size: CGSize(width: 640.0, height: 640.0), fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: nil), completion: { result in guard let item = result.item else { return } var image: UIImage? if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { switch frame.frame.format { case let .rgba(data, width, height, bytesPerRow): guard let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) else { return } data.withUnsafeBytes { bytes -> Void in memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow) } image = context.generateImage() if file.isCustomTemplateEmoji { image = generateTintedImage(image: image, color: .white) } default: return } } Queue.mainQueue().async { guard let image else { return } let size = CGSize(width: 800.0, height: 800.0) let backgroundImage = state.selectedBackground.generateImage(size: size) let tempPath = NSTemporaryDirectory() + "/\(UInt64.random(in: 0 ... UInt64.max)).jpg" let tempUrl = NSURL(fileURLWithPath: tempPath) as URL try? backgroundImage.jpegData(compressionQuality: 0.8)?.write(to: tempUrl) let drawingSize = CGSize(width: 1920.0, height: 1920.0) let entity = DrawingStickerEntity(content: .file(.standalone(media: file), .sticker)) entity.referenceDrawingSize = drawingSize entity.position = CGPoint(x: drawingSize.width / 2.0, y: drawingSize.height / 2.0) entity.scale = 3.3 var fileId: Int64 = 0 var stickerPackId: Int64 = 0 var stickerPackAccessHash: Int64 = 0 if case let .file(fileReference, _) = entity.content { let file = fileReference.media if file.isCustomEmoji { fileId = file.fileId.id } else if file.isAnimatedSticker { for attribute in file.attributes { if case let .Sticker(_, packReference, _) = attribute, let packReference, case let .id(id, accessHash) = packReference { fileId = file.fileId.id stickerPackId = id stickerPackAccessHash = accessHash break } } } } let backgroundColors = state.selectedBackground.colors.map { Int32(bitPattern: $0) } guard let codableEntity = CodableDrawingEntity(entity: entity) else { return } let values = MediaEditorValues( peerId: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), originalDimensions: PixelDimensions(size), cropOffset: .zero, cropRect: CGRect(origin: .zero, size: size), cropScale: 1.0, cropRotation: 0.0, cropMirroring: false, cropOrientation: nil, gradientColors: nil, videoTrimRange: nil, videoBounce: false, videoIsMuted: false, videoIsFullHd: false, videoIsMirrored: false, videoVolume: nil, additionalVideoPath: nil, additionalVideoIsDual: false, additionalVideoMirroringChanges: [], additionalVideoPosition: nil, additionalVideoScale: nil, additionalVideoRotation: nil, additionalVideoPositionChanges: [], additionalVideoTrimRange: nil, additionalVideoOffset: nil, additionalVideoVolume: nil, collage: [], nightTheme: false, drawing: nil, maskDrawing: nil, entities: [codableEntity], toolValues: [:], audioTrack: nil, audioTrackTrimRange: nil, audioTrackOffset: nil, audioTrackVolume: nil, audioTrackSamples: nil, collageTrackSamples: nil, coverImageTimestamp: nil, coverDimensions: nil, qualityPreset: .profileHigh ) let combinedImage = generateImage(size, contextGenerator: { size, context in let bounds = CGRect(origin: .zero, size: size) if let cgImage = backgroundImage.cgImage { context.draw(cgImage, in: bounds) } context.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.scaleBy(x: 0.67, y: 0.67) context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) if let cgImage = image.cgImage { context.draw(cgImage, in: bounds) } }, opaque: false)! if entity.isAnimated { let markup: UploadPeerPhotoMarkup if stickerPackId != 0 { markup = .sticker(packReference: .id(id: stickerPackId, accessHash: stickerPackAccessHash), fileId: fileId, backgroundColors: backgroundColors) } else { markup = .emoji(fileId: fileId, backgroundColors: backgroundColors) } if stickerPackId != 0 { controller.videoCompletion(combinedImage, tempUrl, values, markup, { [weak controller] in controller?.dismiss() }) } else { controller.videoCompletion(combinedImage, tempUrl, values, markup, { [weak controller] in controller?.dismiss() }) } } else { controller.imageCompletion(combinedImage, { [weak controller] in controller?.dismiss() }) } } }) } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } public final class AvatarEditorScreen: ViewControllerComponentContainer { public enum PeerType { case suggest case user case group case channel case forum } fileprivate let context: AccountContext fileprivate let inputData: Signal private let readyValue = Promise() override public var ready: Promise { return self.readyValue } public var imageCompletion: (UIImage, @escaping () -> Void) -> Void = { _, _ in } public var videoCompletion: (UIImage, URL, MediaEditorValues, UploadPeerPhotoMarkup, @escaping () -> Void) -> Void = { _, _, _, _, _ in } public static func inputData(context: AccountContext, isGroup: Bool) -> Signal { let emojiItems = EmojiPagerContentComponent.emojiInputData( context: context, animationCache: context.animationCache, animationRenderer: context.animationRenderer, isStandalone: false, subject: isGroup ? .groupPhoto : .profilePhoto, hasTrending: false, 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, subject: isGroup ? .groupPhotoEmojiSelection : .profilePhotoEmojiSelection ) let signal = combineLatest(queue: .mainQueue(), emojiItems, stickerItems ) |> map { emoji, stickers -> AvatarKeyboardInputData in return AvatarKeyboardInputData(emoji: emoji, stickers: stickers) } return signal } public init(context: AccountContext, inputData: Signal, peerType: PeerType, markup: TelegramMediaImage.EmojiMarkup?) { self.context = context self.inputData = inputData let componentReady = Promise() super.init(context: context, component: AvatarEditorScreenComponent(context: context, ready: componentReady, peerType: peerType, markup: markup), navigationBarAppearance: .transparent) self.automaticallyControlPresentationContextLayout = false self.navigationPresentation = .modal self.readyValue.set(componentReady.get() |> timeout(0.3, queue: .mainQueue(), alternate: .single(true))) self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView()) self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.scrollToTop = { [weak self] in if let self { if let view = self.node.hostView.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View { view.scrollToTop() } else if let view = self.node.hostView.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View { view.scrollToTop() } } } } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override public func viewDidLoad() { super.viewDidLoad() } public override func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? { return CGSize(width: 390.0, height: 730.0) } }