import Foundation import UIKit import Display import SwiftSignalKit import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences import ItemListUI import PresentationDataUtils import AccountContext import StickerPackPreviewUI import ItemListStickerPackItem import UndoUI private final class GroupStickerPackSetupControllerArguments { let context: AccountContext let selectStickerPack: (StickerPackCollectionInfo) -> Void let openStickerPack: (StickerPackCollectionInfo) -> Void let updateSearchText: (String) -> Void let openStickersBot: () -> Void init(context: AccountContext, selectStickerPack: @escaping (StickerPackCollectionInfo) -> Void, openStickerPack: @escaping (StickerPackCollectionInfo) -> Void, updateSearchText: @escaping (String) -> Void, openStickersBot: @escaping () -> Void) { self.context = context self.selectStickerPack = selectStickerPack self.openStickerPack = openStickerPack self.updateSearchText = updateSearchText self.openStickersBot = openStickersBot } } private enum GroupStickerPackSection: Int32 { case search case stickers } private enum GroupStickerPackEntryId: Hashable { case index(Int32) case pack(ItemCollectionId) static func ==(lhs: GroupStickerPackEntryId, rhs: GroupStickerPackEntryId) -> Bool { switch lhs { case let .index(index): if case .index(index) = rhs { return true } else { return false } case let .pack(id): if case .pack(id) = rhs { return true } else { return false } } } } private enum GroupStickerPackEntry: ItemListNodeEntry { case search(PresentationTheme, PresentationStrings, String, String, String) case currentPack(Int32, PresentationTheme, PresentationStrings, GroupStickerPackCurrentItemContent) case searchInfo(PresentationTheme, String) case packsTitle(PresentationTheme, String) case pack(Int32, PresentationTheme, PresentationStrings, StickerPackCollectionInfo, StickerPackItem?, String, Bool, Bool) var section: ItemListSectionId { switch self { case .search, .currentPack, .searchInfo: return GroupStickerPackSection.search.rawValue case .packsTitle, .pack: return GroupStickerPackSection.stickers.rawValue } } var stableId: GroupStickerPackEntryId { switch self { case .search: return .index(0) case .currentPack: return .index(1) case .searchInfo: return .index(2) case .packsTitle: return .index(3) case let .pack(_, _, _, info, _, _, _, _): return .pack(info.id) } } static func ==(lhs: GroupStickerPackEntry, rhs: GroupStickerPackEntry) -> Bool { switch lhs { case let .search(lhsTheme, lhsStrings, lhsPrefix, lhsPlaceholder, lhsValue): if case let .search(rhsTheme, rhsStrings, rhsPrefix, rhsPlaceholder, rhsValue) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPrefix == rhsPrefix, lhsPlaceholder == rhsPlaceholder, lhsValue == rhsValue { return true } else { return false } case let .searchInfo(lhsTheme, lhsText): if case let .searchInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .packsTitle(lhsTheme, lhsText): if case let .packsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .currentPack(lhsIndex, lhsTheme, lhsStrings, lhsContent): if case let .currentPack(rhsIndex, rhsTheme, rhsStrings, rhsContent) = rhs { if lhsIndex != rhsIndex { return false } if lhsTheme !== rhsTheme { return false } if lhsStrings !== rhsStrings { return false } if lhsContent != rhsContent { return false } return true } else { return false } case let .pack(lhsIndex, lhsTheme, lhsStrings, lhsInfo, lhsTopItem, lhsCount, lhsPlayAnimatedStickers, lhsSelected): if case let .pack(rhsIndex, rhsTheme, rhsStrings, rhsInfo, rhsTopItem, rhsCount, rhsPlayAnimatedStickers, rhsSelected) = rhs { if lhsIndex != rhsIndex { return false } if lhsTheme !== rhsTheme { return false } if lhsStrings !== rhsStrings { return false } if lhsInfo != rhsInfo { return false } if lhsTopItem != rhsTopItem { return false } if lhsCount != rhsCount { return false } if lhsPlayAnimatedStickers != rhsPlayAnimatedStickers { return false } if lhsSelected != rhsSelected { return false } return true } else { return false } } } static func <(lhs: GroupStickerPackEntry, rhs: GroupStickerPackEntry) -> Bool { switch lhs { case .search: switch rhs { case .search: return false default: return true } case .currentPack: switch rhs { case .search, .currentPack: return false default: return true } case .searchInfo: switch rhs { case .search, .currentPack, .searchInfo: return false default: return true } case .packsTitle: switch rhs { case .search, .currentPack, .searchInfo, .packsTitle: return false default: return true } case let .pack(lhsIndex, _, _, _, _, _, _, _): switch rhs { case let .pack(rhsIndex, _, _, _, _, _, _, _): return lhsIndex < rhsIndex default: return false } } } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! GroupStickerPackSetupControllerArguments switch self { case let .search(theme, _, prefix, placeholder, value): let isEmoji = prefix.contains("addemoji") return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: prefix, textColor: theme.list.itemPrimaryTextColor), text: value, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), spacing: 0.0, clearType: .none, tag: nil, sectionId: self.section, textUpdated: { value in arguments.updateSearchText(value) }, processPaste: { text in if let url = (URL(string: text) ?? URL(string: "http://" + text)), url.host == "t.me" || url.host == "telegram.me" { let prefix = isEmoji ? "/addemoji/" : "/addstickers/" if url.path.hasPrefix(prefix) { return String(url.path[url.path.index(url.path.startIndex, offsetBy: prefix.count)...]) } } return text }, action: {}) case let .searchInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section, linkAction: nil) case let .packsTitle(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .pack(_, _, _, info, topItem, count, playAnimatedStickers, selected): return ItemListStickerPackItem(presentationData: presentationData, context: arguments.context, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: selected ? .selection : .none, editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false, reorderable: false, selectable: false), enabled: true, playAnimatedStickers: playAnimatedStickers, sectionId: self.section, action: { if selected { arguments.openStickerPack(info) } else { arguments.selectStickerPack(info) } }, setPackIdWithRevealedOptions: { _, _ in }, addPack: { }, removePack: { }, toggleSelected: { }) case let .currentPack(_, theme, strings, content): return GroupStickerPackCurrentItem(theme: theme, strings: strings, account: arguments.context.account, content: content, sectionId: self.section, action: { if case let .found(packInfo, _, _) = content { arguments.openStickerPack(packInfo) } }, remove: { arguments.updateSearchText("") }) } } } private struct StickerPackData: Equatable { let info: StickerPackCollectionInfo let item: StickerPackItem? } private enum InitialStickerPackData { case noData case data(StickerPackData) } private enum GroupStickerPackSearchState: Equatable { case none case found(StickerPackData) case notFound case searching } private struct GroupStickerPackSetupControllerState: Equatable { var isSaving: Bool var searchingPacks: Bool } private func groupStickerPackSetupControllerEntries(context: AccountContext, presentationData: PresentationData, searchText: String, view: CombinedView, initialData: InitialStickerPackData?, searchState: GroupStickerPackSearchState, stickerSettings: StickerSettings, isEmoji: Bool) -> [GroupStickerPackEntry] { if initialData == nil { return [] } var entries: [GroupStickerPackEntry] = [] entries.append(.search(presentationData.theme, presentationData.strings, isEmoji ? "t.me/addemoji/" : "t.me/addstickers/", isEmoji ? "emojiset" : presentationData.strings.Channel_Stickers_Placeholder, searchText)) switch searchState { case .none: break case .notFound: entries.append(.currentPack(0, presentationData.theme, presentationData.strings, .notFound(isEmoji: isEmoji))) case .searching: entries.append(.currentPack(0, presentationData.theme, presentationData.strings, .searching)) case let .found(data): entries.append(.currentPack(0, presentationData.theme, presentationData.strings, .found(packInfo: data.info, topItem: data.item, subtitle: isEmoji ? presentationData.strings.StickerPack_EmojiCount(data.info.count) : presentationData.strings.StickerPack_StickerCount(data.info.count)))) } entries.append(.searchInfo(presentationData.theme, isEmoji ? presentationData.strings.Group_Emoji_Info : presentationData.strings.Channel_Stickers_CreateYourOwn)) let namespace = isEmoji ? Namespaces.ItemCollection.CloudEmojiPacks : Namespaces.ItemCollection.CloudStickerPacks if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [namespace])] as? ItemCollectionInfosView { if let packsEntries = stickerPacksView.entriesByNamespace[namespace] { if !packsEntries.isEmpty { entries.append(.packsTitle(presentationData.theme, isEmoji ? presentationData.strings.Group_Emoji_YourEmoji : presentationData.strings.Channel_Stickers_YourStickers)) } var index: Int32 = 0 for entry in packsEntries { if let info = entry.info as? StickerPackCollectionInfo { var selected = false if case let .found(found) = searchState { selected = found.info.id == info.id } let count = info.count == 0 ? entry.count : info.count let thumbnail: StickerPackItem? if let thumbnailRep = info.thumbnail { thumbnail = StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: thumbnailRep.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: info.immediateThumbnailData, mimeType: "", size: nil, attributes: []), indexKeys: []) } else { thumbnail = entry.firstItem as? StickerPackItem } entries.append(.pack(index, presentationData.theme, presentationData.strings, info, thumbnail, isEmoji ? presentationData.strings.StickerPack_EmojiCount(count) : presentationData.strings.StickerPack_StickerCount(count), context.sharedContext.energyUsageSettings.loopStickers, selected)) index += 1 } } } } return entries } public func groupStickerPackSetupController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: PeerId, isEmoji: Bool = false, currentPackInfo: StickerPackCollectionInfo?, completion: ((StickerPackCollectionInfo?) -> Void)? = nil) -> ViewController { let initialState = GroupStickerPackSetupControllerState(isSaving: false, searchingPacks: false) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((GroupStickerPackSetupControllerState) -> GroupStickerPackSetupControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } let searchText = ValuePromise(currentPackInfo?.shortName ?? "", ignoreRepeated: true) let initialData = Promise() if let currentPackInfo = currentPackInfo { initialData.set(context.engine.stickers.cachedStickerPack(reference: .id(id: currentPackInfo.id.id, accessHash: currentPackInfo.accessHash), forceRemote: false) |> map { result -> InitialStickerPackData? in switch result { case .none: return .noData case .fetching: return nil case let .result(info, items, _): return InitialStickerPackData.data(StickerPackData(info: info, item: items.first)) } }) } else { initialData.set(.single(.noData)) } var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? var completionImpl: ((StickerPackCollectionInfo?) -> Void)? if let completion { completionImpl = { value in completion(value) } } let stickerPacks = Promise() stickerPacks.set(context.account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [isEmoji ? Namespaces.ItemCollection.CloudEmojiPacks : Namespaces.ItemCollection.CloudStickerPacks])])) let searchState = Promise<(String, GroupStickerPackSearchState)>() searchState.set(combineLatest(searchText.get(), initialData.get(), stickerPacks.get()) |> mapToSignal { searchText, initialData, view -> Signal<(String, GroupStickerPackSearchState), NoError> in if let initialData = initialData { if searchText.isEmpty { return .single((searchText, .none)) } else if case let .data(data) = initialData, searchText.lowercased() == data.info.shortName { Queue.mainQueue().async { completionImpl?(data.info) } return .single((searchText, .found(StickerPackData(info: data.info, item: data.item)))) } else { let namespace = isEmoji ? Namespaces.ItemCollection.CloudEmojiPacks : Namespaces.ItemCollection.CloudStickerPacks if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [namespace])] as? ItemCollectionInfosView { if let packsEntries = stickerPacksView.entriesByNamespace[namespace] { for entry in packsEntries { if let info = entry.info as? StickerPackCollectionInfo { if info.shortName.lowercased() == searchText.lowercased() { return .single((searchText, .found(StickerPackData(info: info, item: entry.firstItem as? StickerPackItem)))) } } } } } return .single((searchText, .searching)) |> then((context.engine.stickers.loadedStickerPack(reference: .name(searchText.lowercased()), forceActualized: false) |> delay(0.3, queue: Queue.concurrentDefaultQueue())) |> mapToSignal { value -> Signal<(String, GroupStickerPackSearchState), NoError> in switch value { case .fetching: return .complete() case .none: return .single((searchText, .notFound)) case let .result(info, items, _): return .single((searchText, .found(StickerPackData(info: info, item: items.first)))) } } |> afterNext { value in if case let .found(data) = value.1 { Queue.mainQueue().async { completionImpl?(data.info) } } }) } } else { return .single((searchText, .none)) } }) var navigateToChatControllerImpl: ((PeerId) -> Void)? var dismissInputImpl: (() -> Void)? var dismissImpl: (() -> Void)? let actionsDisposable = DisposableSet() let resolveDisposable = MetaDisposable() actionsDisposable.add(resolveDisposable) let saveDisposable = MetaDisposable() actionsDisposable.add(saveDisposable) var presentStickerPackController: ((StickerPackCollectionInfo) -> Void)? let arguments = GroupStickerPackSetupControllerArguments(context: context, selectStickerPack: { info in searchText.set(info.shortName) completionImpl?(info) }, openStickerPack: { info in presentStickerPackController?(info) }, updateSearchText: { text in searchText.set(text) if text == "" { completionImpl?(nil) } }, openStickersBot: { resolveDisposable.set((context.engine.peers.resolvePeerByName(name: "stickers") |> mapToSignal { result -> Signal in guard case let .result(result) = result else { return .complete() } return .single(result) } |> deliverOnMainQueue).start(next: { peer in if let peer = peer { dismissImpl?() navigateToChatControllerImpl?(peer.id) } })) }) let previousHadData = Atomic(value: false) let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData let signal = combineLatest(presentationData, statePromise.get() |> deliverOnMainQueue, initialData.get() |> deliverOnMainQueue, stickerPacks.get() |> deliverOnMainQueue, searchState.get() |> deliverOnMainQueue, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings]) |> deliverOnMainQueue) |> map { presentationData, state, initialData, view, searchState, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in var stickerSettings = StickerSettings.defaultSettings if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.stickerSettings]?.get(StickerSettings.self) { stickerSettings = value } let leftNavigationButton: ItemListNavigationButton? if isEmoji { leftNavigationButton = nil } else { leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) } var rightNavigationButton: ItemListNavigationButton? if let _ = completion { rightNavigationButton = ItemListNavigationButton(content: .icon(.search), style: .regular, enabled: true, action: { updateState { state in var updatedState = state updatedState.searchingPacks = true return updatedState } }) } else { if initialData != nil { if state.isSaving { rightNavigationButton = ItemListNavigationButton(content: .text(""), style: .activity, enabled: true, action: {}) } else { let enabled: Bool var info: StickerPackCollectionInfo? switch searchState.1 { case .searching, .notFound: enabled = false case .none: enabled = true case let .found(data): enabled = true info = data.info } rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: enabled, action: { if info?.id == currentPackInfo?.id { dismissImpl?() } else { updateState { state in var state = state state.isSaving = true return state } saveDisposable.set((context.engine.peers.updateGroupSpecificStickerset(peerId: peerId, info: info) |> deliverOnMainQueue).start(error: { _ in updateState { state in var state = state state.isSaving = false return state } }, completed: { dismissImpl?() })) } }) } } } var searchItem: ItemListControllerSearch? if state.searchingPacks { searchItem = GroupStickerSearchItem(context: context, cancel: { updateState { state in var updatedState = state updatedState.searchingPacks = false return updatedState } }, select: { pack in arguments.selectStickerPack(pack) }, dismissInput: { dismissInputImpl?() }) } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(isEmoji ? presentationData.strings.Group_Emoji_Title : presentationData.strings.Channel_Info_Stickers), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) let hasData = initialData != nil let hadData = previousHadData.swap(hasData) var emptyStateItem: ItemListLoadingIndicatorEmptyStateItem? if !hasData { emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: groupStickerPackSetupControllerEntries(context: context, presentationData: presentationData, searchText: searchState.0, view: view, initialData: initialData, searchState: searchState.1, stickerSettings: stickerSettings, isEmoji: isEmoji), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, animateChanges: hasData && hadData) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } let controller = ItemListController(context: context, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { if c is UndoOverlayController { controller.window?.forEachController { c in if let controller = c as? UndoOverlayController { controller.dismiss() } } } controller.present(c, in: .window(.root), with: p) } } dismissInputImpl = { [weak controller] in controller?.view.endEditing(true) } presentStickerPackController = { [weak controller] info in dismissInputImpl?() let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) presentControllerImpl?(StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controller?.navigationController as? NavigationController), nil) } navigateToChatControllerImpl = { [weak controller] peerId in let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { peer in guard let peer = peer else { return } if let controller = controller, let navigationController = controller.navigationController as? NavigationController { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) } }) } dismissImpl = { [weak controller] in dismissInputImpl?() controller?.dismiss() } return controller }