mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 22:55:00 +00:00
Group boosts
This commit is contained in:
@@ -20,6 +20,9 @@ swift_library(
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/StickerPackPreviewUI:StickerPackPreviewUI",
|
||||
"//submodules/ItemListStickerPackItem:ItemListStickerPackItem",
|
||||
"//submodules/SearchBarNode:SearchBarNode",
|
||||
"//submodules/SearchUI:SearchUI",
|
||||
"//submodules/MergeLists:MergeLists",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -259,6 +259,7 @@ private enum GroupStickerPackSearchState: Equatable {
|
||||
|
||||
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, emoji: Bool) -> [GroupStickerPackEntry] {
|
||||
@@ -276,10 +277,10 @@ private func groupStickerPackSetupControllerEntries(context: AccountContext, pre
|
||||
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: presentationData.strings.StickerPack_StickerCount(data.info.count))))
|
||||
entries.append(.currentPack(0, presentationData.theme, presentationData.strings, .found(packInfo: data.info, topItem: data.item, subtitle: emoji ? presentationData.strings.StickerPack_EmojiCount(data.info.count) : presentationData.strings.StickerPack_StickerCount(data.info.count))))
|
||||
}
|
||||
entries.append(.searchInfo(presentationData.theme, emoji ? "All members will be able to use these emoji in the group, even if they don't have Telegram Premium." : presentationData.strings.Channel_Stickers_CreateYourOwn))
|
||||
entries.append(.packsTitle(presentationData.theme, emoji ? "CHOOSE FROM YOUR EMOJI" : presentationData.strings.Channel_Stickers_YourStickers))
|
||||
entries.append(.packsTitle(presentationData.theme, emoji ? "CHOOSE EMOJI PACK" : presentationData.strings.Channel_Stickers_YourStickers))
|
||||
|
||||
let namespace = emoji ? Namespaces.ItemCollection.CloudEmojiPacks : Namespaces.ItemCollection.CloudStickerPacks
|
||||
if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [namespace])] as? ItemCollectionInfosView {
|
||||
@@ -291,7 +292,15 @@ private func groupStickerPackSetupControllerEntries(context: AccountContext, pre
|
||||
if case let .found(found) = searchState {
|
||||
selected = found.info.id == info.id
|
||||
}
|
||||
entries.append(.pack(index, presentationData.theme, presentationData.strings, info, entry.firstItem as? StickerPackItem, presentationData.strings.StickerPack_StickerCount(info.count == 0 ? entry.count : info.count), context.sharedContext.energyUsageSettings.loopStickers, selected))
|
||||
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, emoji ? presentationData.strings.StickerPack_EmojiCount(count) : presentationData.strings.StickerPack_StickerCount(count), context.sharedContext.energyUsageSettings.loopStickers, selected))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
@@ -302,7 +311,7 @@ private func groupStickerPackSetupControllerEntries(context: AccountContext, pre
|
||||
}
|
||||
|
||||
public func groupStickerPackSetupController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, emoji: Bool = false, currentPackInfo: StickerPackCollectionInfo?, completion: ((StickerPackCollectionInfo?) -> Void)? = nil) -> ViewController {
|
||||
let initialState = GroupStickerPackSetupControllerState(isSaving: false)
|
||||
let initialState = GroupStickerPackSetupControllerState(isSaving: false, searchingPacks: false)
|
||||
|
||||
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: initialState)
|
||||
@@ -433,10 +442,15 @@ public func groupStickerPackSetupController(context: AccountContext, updatedPres
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
var rightNavigationButton: ItemListNavigationButton?
|
||||
if let _ = completion {
|
||||
rightNavigationButton = ItemListNavigationButton(content: .icon(.search), style: .regular, enabled: true, action: {})
|
||||
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 {
|
||||
@@ -467,7 +481,7 @@ public func groupStickerPackSetupController(context: AccountContext, updatedPres
|
||||
return state
|
||||
}
|
||||
saveDisposable.set((context.engine.peers.updateGroupSpecificStickerset(peerId: peerId, info: info)
|
||||
|> deliverOnMainQueue).start(error: { _ in
|
||||
|> deliverOnMainQueue).start(error: { _ in
|
||||
updateState { state in
|
||||
var state = state
|
||||
state.isSaving = false
|
||||
@@ -483,6 +497,21 @@ public func groupStickerPackSetupController(context: AccountContext, updatedPres
|
||||
}
|
||||
}
|
||||
|
||||
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(emoji ? "Group Emoji Pack" : presentationData.strings.Channel_Info_Stickers), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
|
||||
|
||||
let hasData = initialData != nil
|
||||
@@ -493,7 +522,7 @@ public func groupStickerPackSetupController(context: AccountContext, updatedPres
|
||||
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, emoji: emoji), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: hasData && hadData)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: groupStickerPackSetupControllerEntries(context: context, presentationData: presentationData, searchText: searchState.0, view: view, initialData: initialData, searchState: searchState.1, stickerSettings: stickerSettings, emoji: emoji), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, animateChanges: hasData && hadData)
|
||||
return (controllerState, (listState, arguments))
|
||||
} |> afterDisposed {
|
||||
actionsDisposable.dispose()
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import MergeLists
|
||||
import AccountContext
|
||||
import SearchUI
|
||||
import ItemListUI
|
||||
import ItemListStickerPackItem
|
||||
|
||||
private final class GroupStickerSearchContainerInteraction {
|
||||
let packSelected: (StickerPackCollectionInfo) -> Void
|
||||
|
||||
init(packSelected: @escaping (StickerPackCollectionInfo) -> Void) {
|
||||
self.packSelected = packSelected
|
||||
}
|
||||
}
|
||||
|
||||
private final class GroupStickerSearchEntry: Comparable, Identifiable {
|
||||
let index: Int
|
||||
let pack: StickerPackCollectionInfo
|
||||
let topItem: ItemCollectionItem?
|
||||
|
||||
init(index: Int, pack: StickerPackCollectionInfo, topItem: ItemCollectionItem?) {
|
||||
self.index = index
|
||||
self.pack = pack
|
||||
self.topItem = topItem
|
||||
}
|
||||
|
||||
var stableId: ItemCollectionId {
|
||||
return self.pack.id
|
||||
}
|
||||
|
||||
static func ==(lhs: GroupStickerSearchEntry, rhs: GroupStickerSearchEntry) -> Bool {
|
||||
return lhs.index == rhs.index && lhs.pack == rhs.pack
|
||||
}
|
||||
|
||||
static func <(lhs: GroupStickerSearchEntry, rhs: GroupStickerSearchEntry) -> Bool {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
|
||||
func item(context: AccountContext, presentationData: PresentationData, interaction: GroupStickerSearchContainerInteraction) -> ListViewItem {
|
||||
let pack = self.pack
|
||||
let count = presentationData.strings.StickerPack_EmojiCount(pack.count)
|
||||
|
||||
return ItemListStickerPackItem(presentationData: ItemListPresentationData(presentationData), context: context, packInfo: pack, itemCount: count, topItem: self.topItem as? StickerPackItem, unread: false, control: .none, editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false, reorderable: false, selectable: false), enabled: true, playAnimatedStickers: true, style: .plain, sectionId: 0, action: {
|
||||
interaction.packSelected(pack)
|
||||
}, setPackIdWithRevealedOptions: { _, _ in
|
||||
}, addPack: {
|
||||
}, removePack: {
|
||||
}, toggleSelected: {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupStickerSearchContainerTransition {
|
||||
let deletions: [ListViewDeleteItem]
|
||||
let insertions: [ListViewInsertItem]
|
||||
let updates: [ListViewUpdateItem]
|
||||
let isSearching: Bool
|
||||
let isEmpty: Bool
|
||||
let query: String
|
||||
}
|
||||
|
||||
private func groupStickerSearchContainerPreparedRecentTransition(from fromEntries: [GroupStickerSearchEntry], to toEntries: [GroupStickerSearchEntry], isSearching: Bool, isEmpty: Bool, query: String, context: AccountContext, presentationData: PresentationData, interaction: GroupStickerSearchContainerInteraction) -> GroupStickerSearchContainerTransition {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) }
|
||||
|
||||
return GroupStickerSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching, isEmpty: isEmpty, query: query)
|
||||
}
|
||||
|
||||
public final class GroupStickerSearchContainerNode: SearchDisplayControllerContentNode {
|
||||
private let context: AccountContext
|
||||
private let packSelected: (StickerPackCollectionInfo) -> Void
|
||||
|
||||
private let listNode: ListView
|
||||
|
||||
private let emptyResultsTitleNode: ImmediateTextNode
|
||||
private let emptyResultsTextNode: ImmediateTextNode
|
||||
|
||||
private var enqueuedTransitions: [(GroupStickerSearchContainerTransition, Bool)] = []
|
||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
private let searchQuery = Promise<String?>()
|
||||
private let searchDisposable = MetaDisposable()
|
||||
|
||||
private let forceTheme: PresentationTheme?
|
||||
private var presentationData: PresentationData
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
private let removeMemberDisposable = MetaDisposable()
|
||||
|
||||
private let presentationDataPromise: Promise<PresentationData>
|
||||
|
||||
private var _hasDim: Bool = false
|
||||
override public var hasDim: Bool {
|
||||
return _hasDim
|
||||
}
|
||||
|
||||
public init(context: AccountContext, forceTheme: PresentationTheme?, packSelected: @escaping (StickerPackCollectionInfo) -> Void, updateActivity: @escaping (Bool) -> Void, pushController: @escaping (ViewController) -> Void) {
|
||||
self.context = context
|
||||
self.packSelected = packSelected
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.presentationData = presentationData
|
||||
|
||||
self.forceTheme = forceTheme
|
||||
if let forceTheme = self.forceTheme {
|
||||
self.presentationData = self.presentationData.withUpdated(theme: forceTheme)
|
||||
}
|
||||
self.presentationDataPromise = Promise(self.presentationData)
|
||||
|
||||
self.listNode = ListView()
|
||||
self.listNode.accessibilityPageScrolledString = { row, count in
|
||||
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
|
||||
}
|
||||
|
||||
self.emptyResultsTitleNode = ImmediateTextNode()
|
||||
self.emptyResultsTitleNode.displaysAsynchronously = false
|
||||
self.emptyResultsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.ChatList_Search_NoResults, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.freeTextColor)
|
||||
self.emptyResultsTitleNode.textAlignment = .center
|
||||
self.emptyResultsTitleNode.isHidden = true
|
||||
|
||||
self.emptyResultsTextNode = ImmediateTextNode()
|
||||
self.emptyResultsTextNode.displaysAsynchronously = false
|
||||
self.emptyResultsTextNode.maximumNumberOfLines = 0
|
||||
self.emptyResultsTextNode.textAlignment = .center
|
||||
self.emptyResultsTextNode.isHidden = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor
|
||||
self.listNode.isHidden = true
|
||||
|
||||
self._hasDim = true
|
||||
|
||||
self.addSubnode(self.listNode)
|
||||
|
||||
self.addSubnode(self.emptyResultsTitleNode)
|
||||
self.addSubnode(self.emptyResultsTextNode)
|
||||
|
||||
let interaction = GroupStickerSearchContainerInteraction(packSelected: { [weak self] pack in
|
||||
packSelected(pack)
|
||||
self?.listNode.clearHighlightAnimated(true)
|
||||
})
|
||||
|
||||
let foundItems = self.searchQuery.get()
|
||||
|> mapToSignal { query -> Signal<[GroupStickerSearchEntry]?, NoError> in
|
||||
guard let query, !query.isEmpty else {
|
||||
return .single(nil)
|
||||
}
|
||||
return context.engine.stickers.searchEmojiSets(query: query)
|
||||
|> mapToSignal { localResult in
|
||||
return context.engine.stickers.searchEmojiSetsRemotely(query: query)
|
||||
|> map { remoteResult -> [GroupStickerSearchEntry]? in
|
||||
let mergedResult = localResult.merge(with: remoteResult)
|
||||
var entries: [GroupStickerSearchEntry] = []
|
||||
var index = 0
|
||||
for info in mergedResult.infos {
|
||||
if let pack = info.1 as? StickerPackCollectionInfo {
|
||||
entries.append(GroupStickerSearchEntry(index: index, pack: pack, topItem: info.2))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let previousSearchItems = Atomic<[GroupStickerSearchEntry]?>(value: nil)
|
||||
self.searchDisposable.set((combineLatest(self.searchQuery.get(), foundItems, self.presentationDataPromise.get())
|
||||
|> deliverOnMainQueue).start(next: { [weak self] query, entries, presentationData in
|
||||
if let strongSelf = self {
|
||||
let previousEntries = previousSearchItems.swap(entries)
|
||||
updateActivity(false)
|
||||
let firstTime = previousEntries == nil
|
||||
let transition = groupStickerSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, isEmpty: entries?.isEmpty ?? false, query: query ?? "", context: context, presentationData: presentationData, interaction: interaction)
|
||||
strongSelf.enqueueTransition(transition, firstTime: firstTime)
|
||||
}
|
||||
}))
|
||||
|
||||
self.presentationDataDisposable = (context.sharedContext.presentationData
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
|
||||
if let strongSelf = self {
|
||||
var presentationData = presentationData
|
||||
|
||||
let previousTheme = strongSelf.presentationData.theme
|
||||
let previousStrings = strongSelf.presentationData.strings
|
||||
|
||||
if let forceTheme = strongSelf.forceTheme {
|
||||
presentationData = presentationData.withUpdated(theme: forceTheme)
|
||||
}
|
||||
|
||||
strongSelf.presentationData = presentationData
|
||||
|
||||
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
|
||||
strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.listNode.beganInteractiveDragging = { [weak self] _ in
|
||||
self?.dismissInput?()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.searchDisposable.dispose()
|
||||
self.presentationDataDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.listNode.backgroundColor = theme.chatList.backgroundColor
|
||||
}
|
||||
|
||||
override public func searchTextUpdated(text: String) {
|
||||
if text.isEmpty {
|
||||
self.searchQuery.set(.single(nil))
|
||||
} else {
|
||||
self.searchQuery.set(.single(text))
|
||||
}
|
||||
}
|
||||
|
||||
private func enqueueTransition(_ transition: GroupStickerSearchContainerTransition, firstTime: Bool) {
|
||||
enqueuedTransitions.append((transition, firstTime))
|
||||
|
||||
if let _ = self.validLayout {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
self.dequeueTransition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func dequeueTransition() {
|
||||
if let (transition, firstTime) = self.enqueuedTransitions.first {
|
||||
self.enqueuedTransitions.remove(at: 0)
|
||||
|
||||
var options = ListViewDeleteAndInsertOptions()
|
||||
options.insert(.PreferSynchronousDrawing)
|
||||
options.insert(.PreferSynchronousResourceLoading)
|
||||
if firstTime {
|
||||
} else {
|
||||
//options.insert(.AnimateAlpha)
|
||||
}
|
||||
|
||||
let isSearching = transition.isSearching
|
||||
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.listNode.isHidden = !isSearching
|
||||
|
||||
strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(transition.query).string, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor)
|
||||
|
||||
let emptyResults = transition.isSearching && transition.isEmpty
|
||||
strongSelf.emptyResultsTitleNode.isHidden = !emptyResults
|
||||
strongSelf.emptyResultsTextNode.isHidden = !emptyResults
|
||||
|
||||
if let (layout, navigationBarHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
|
||||
|
||||
let hadValidLayout = self.validLayout == nil
|
||||
self.validLayout = (layout, navigationBarHeight)
|
||||
|
||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
||||
|
||||
var insets = layout.insets(options: [.input])
|
||||
insets.top += navigationBarHeight
|
||||
insets.left += layout.safeInsets.left
|
||||
insets.right += layout.safeInsets.right
|
||||
|
||||
self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
let padding: CGFloat = 16.0
|
||||
let emptyTitleSize = self.emptyResultsTitleNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude))
|
||||
let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude))
|
||||
|
||||
let emptyTextSpacing: CGFloat = 8.0
|
||||
let emptyTotalHeight = emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing
|
||||
let emptyTitleY = navigationBarHeight + floorToScreenPixels((layout.size.height - navigationBarHeight - max(insets.bottom, layout.intrinsicInsets.bottom) - emptyTotalHeight) / 2.0)
|
||||
|
||||
transition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyTitleY), size: emptyTitleSize))
|
||||
transition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyTitleY + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize))
|
||||
|
||||
if !hadValidLayout {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
self.dequeueTransition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func scrollToTop() {
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
}
|
||||
|
||||
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.cancel?()
|
||||
}
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
guard let result = self.view.hitTest(point, with: event) else {
|
||||
return nil
|
||||
}
|
||||
if result === self.view {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
|
||||
final class GroupStickerSearchItem: ItemListControllerSearch {
|
||||
let context: AccountContext
|
||||
let cancel: () -> Void
|
||||
let select: (StickerPackCollectionInfo) -> Void
|
||||
let dismissInput: () -> Void
|
||||
|
||||
private var updateActivity: ((Bool) -> Void)?
|
||||
private var activity: ValuePromise<Bool> = ValuePromise(ignoreRepeated: false)
|
||||
private let activityDisposable = MetaDisposable()
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
cancel: @escaping () -> Void,
|
||||
select: @escaping (StickerPackCollectionInfo) -> Void,
|
||||
dismissInput: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.cancel = cancel
|
||||
self.select = select
|
||||
self.dismissInput = dismissInput
|
||||
self.activityDisposable.set((self.activity.get() |> mapToSignal { value -> Signal<Bool, NoError> in
|
||||
if value {
|
||||
return .single(value) |> delay(0.2, queue: Queue.mainQueue())
|
||||
} else {
|
||||
return .single(value)
|
||||
}
|
||||
}).start(next: { [weak self] value in
|
||||
self?.updateActivity?(value)
|
||||
}))
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.activityDisposable.dispose()
|
||||
}
|
||||
|
||||
func isEqual(to: ItemListControllerSearch) -> Bool {
|
||||
if let to = to as? GroupStickerSearchItem {
|
||||
if self.context !== to.context {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func titleContentNode(current: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> NavigationBarContentNode & ItemListControllerSearchNavigationContentNode {
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
if let current = current as? GroupStickerSearchNavigationContentNode {
|
||||
current.updateTheme(presentationData.theme)
|
||||
return current
|
||||
} else {
|
||||
return GroupStickerSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, cancel: self.cancel, updateActivity: { [weak self] value in
|
||||
self?.updateActivity = value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func node(current: ItemListControllerSearchNode?, titleContentNode: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> ItemListControllerSearchNode {
|
||||
return GroupStickerSearchItemNode(context: self.context, packSelected: self.select, cancel: self.cancel, updateActivity: { [weak self] value in
|
||||
self?.activity.set(value)
|
||||
}, pushController: { c in
|
||||
|
||||
}, dismissInput: self.dismissInput)
|
||||
}
|
||||
}
|
||||
|
||||
private final class GroupStickerSearchItemNode: ItemListControllerSearchNode {
|
||||
private let containerNode: GroupStickerSearchContainerNode
|
||||
|
||||
init(context: AccountContext, packSelected: @escaping (StickerPackCollectionInfo) -> Void, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, pushController: @escaping (ViewController) -> Void, dismissInput: @escaping () -> Void) {
|
||||
self.containerNode = GroupStickerSearchContainerNode(context: context, forceTheme: nil, packSelected: { pack in
|
||||
packSelected(pack)
|
||||
cancel()
|
||||
}, updateActivity: updateActivity, pushController: pushController)
|
||||
self.containerNode.cancel = {
|
||||
cancel()
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.containerNode)
|
||||
|
||||
self.containerNode.dismissInput = {
|
||||
dismissInput()
|
||||
}
|
||||
}
|
||||
|
||||
override func queryUpdated(_ query: String) {
|
||||
self.containerNode.searchTextUpdated(text: query)
|
||||
}
|
||||
|
||||
override func scrollToTop() {
|
||||
self.containerNode.scrollToTop()
|
||||
}
|
||||
|
||||
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight)))
|
||||
self.containerNode.containerLayoutUpdated(layout.withUpdatedSize(CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight)), navigationBarHeight: 0.0, transition: transition)
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let result = self.containerNode.hitTest(self.view.convert(point, to: self.containerNode.view), with: event) {
|
||||
return result
|
||||
}
|
||||
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import SearchBarNode
|
||||
|
||||
private let searchBarFont = Font.regular(17.0)
|
||||
|
||||
final class GroupStickerSearchNavigationContentNode: NavigationBarContentNode, ItemListControllerSearchNavigationContentNode {
|
||||
private var theme: PresentationTheme
|
||||
private let strings: PresentationStrings
|
||||
|
||||
private let cancel: () -> Void
|
||||
|
||||
private let searchBar: SearchBarNode
|
||||
|
||||
private var queryUpdated: ((String) -> Void)?
|
||||
var activity: Bool = false {
|
||||
didSet {
|
||||
self.searchBar.activity = activity
|
||||
}
|
||||
}
|
||||
init(theme: PresentationTheme, strings: PresentationStrings, cancel: @escaping () -> Void, updateActivity: @escaping(@escaping(Bool)->Void) -> Void) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
|
||||
self.cancel = cancel
|
||||
|
||||
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern, displayBackground: false)
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.searchBar)
|
||||
|
||||
self.searchBar.cancel = { [weak self] in
|
||||
self?.searchBar.deactivate(clear: false)
|
||||
self?.cancel()
|
||||
}
|
||||
|
||||
self.searchBar.textUpdated = { [weak self] query, _ in
|
||||
self?.queryUpdated?(query)
|
||||
}
|
||||
|
||||
updateActivity({ [weak self] value in
|
||||
self?.activity = value
|
||||
})
|
||||
|
||||
self.updatePlaceholder()
|
||||
}
|
||||
|
||||
func setQueryUpdated(_ f: @escaping (String) -> Void) {
|
||||
self.queryUpdated = f
|
||||
}
|
||||
|
||||
func updateTheme(_ theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: self.theme), strings: self.strings)
|
||||
self.updatePlaceholder()
|
||||
}
|
||||
|
||||
func updatePlaceholder() {
|
||||
self.searchBar.placeholderString = NSAttributedString(string: self.strings.Common_Search, font: searchBarFont, textColor: self.theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
|
||||
}
|
||||
|
||||
override var nominalHeight: CGFloat {
|
||||
return 54.0
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - self.nominalHeight), size: CGSize(width: size.width, height: 54.0))
|
||||
self.searchBar.frame = searchBarFrame
|
||||
self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition)
|
||||
}
|
||||
|
||||
func activate() {
|
||||
self.searchBar.activate()
|
||||
}
|
||||
|
||||
func deactivate() {
|
||||
self.searchBar.deactivate(clear: false)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user