411 lines
18 KiB
Swift
411 lines
18 KiB
Swift
//
|
|
// GroupStickerSetController.swift
|
|
// Telegram
|
|
//
|
|
// Created by keepcoder on 24/08/2017.
|
|
// Copyright © 2017 Telegram. All rights reserved.
|
|
//
|
|
|
|
import Cocoa
|
|
import TGUIKit
|
|
import TelegramCore
|
|
|
|
import Postbox
|
|
import SwiftSignalKit
|
|
|
|
private final class GroupStickersArguments {
|
|
let context: AccountContext
|
|
let textChanged:(String, String)->Void
|
|
let installStickerset:((StickerPackCollectionInfo, [ItemCollectionItem], Int32))->Void
|
|
let openChat:(PeerId)->Void
|
|
init(context: AccountContext, textChanged:@escaping(String, String)->Void, installStickerset:@escaping((StickerPackCollectionInfo, [ItemCollectionItem], Int32))->Void, openChat:@escaping(PeerId)->Void) {
|
|
self.context = context
|
|
self.textChanged = textChanged
|
|
self.installStickerset = installStickerset
|
|
self.openChat = openChat
|
|
}
|
|
}
|
|
|
|
private enum GroupStickersetEntryId: Hashable {
|
|
case section(Int32)
|
|
case input
|
|
case status
|
|
case description(Int32)
|
|
case pack(ItemCollectionId)
|
|
|
|
var hashValue: Int {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
private enum GroupStickersetLoadingStatus : Equatable {
|
|
case loaded(StickerPackCollectionInfo, StickerPackItem?, Int32)
|
|
case loading
|
|
case failed
|
|
}
|
|
|
|
private enum GroupStickersetEntry : TableItemListNodeEntry {
|
|
case section(Int32)
|
|
case input(Int32, value: String, viewType: GeneralViewType)
|
|
case status(Int32, status:GroupStickersetLoadingStatus, viewType: GeneralViewType)
|
|
case description(Int32, Int32, text: String, viewType: GeneralViewType)
|
|
case pack(Int32, Int32, StickerPackCollectionInfo, StickerPackItem?, Int32, Bool, GeneralViewType)
|
|
|
|
func withUpdatedViewType(_ viewType: GeneralViewType) -> GroupStickersetEntry {
|
|
switch self {
|
|
case .section: return self
|
|
case let .input(sectionId, value: value, _): return .input(sectionId, value: value, viewType: viewType)
|
|
case let .status(sectionId, status: status, _): return .status(sectionId, status: status, viewType: viewType)
|
|
case let .description(sectionId, index, text: text, _): return .description(sectionId, index, text: text, viewType: viewType)
|
|
case let .pack(sectionId, index, info, item, count, selected, _): return .pack(sectionId, index, info, item, count, selected, viewType)
|
|
}
|
|
}
|
|
|
|
var stableId: GroupStickersetEntryId {
|
|
switch self {
|
|
case .section(let id):
|
|
return .section(id)
|
|
case .input:
|
|
return .input
|
|
case .status:
|
|
return .status
|
|
case .description(_, let id, _, _):
|
|
return .description(id)
|
|
case .pack(_, _, let info, _, _, _, _):
|
|
return .pack(info.id)
|
|
}
|
|
}
|
|
|
|
var stableIndex:Int32 {
|
|
switch self {
|
|
case .input:
|
|
return 0
|
|
case .status:
|
|
return 1
|
|
case .description(_, let index, _, _):
|
|
return 2 + index
|
|
case .pack:
|
|
fatalError("")
|
|
case let .section(sectionId):
|
|
return (sectionId + 1) * 1000 - sectionId
|
|
}
|
|
}
|
|
|
|
var index:Int32 {
|
|
switch self {
|
|
case let .input(sectionId, _, _):
|
|
return (sectionId * 1000) + stableIndex
|
|
case let .status(sectionId, _, _):
|
|
return (sectionId * 1000) + stableIndex
|
|
case let .description(sectionId, _, _, _):
|
|
return (sectionId * 1000) + stableIndex
|
|
case let .pack( sectionId, index, _, _, _, _, _):
|
|
return (sectionId * 1000) + 100 + index
|
|
case let .section(sectionId):
|
|
return (sectionId + 1) * 1000 - sectionId
|
|
}
|
|
}
|
|
|
|
static func <(lhs: GroupStickersetEntry, rhs: GroupStickersetEntry) -> Bool {
|
|
return lhs.index < rhs.index
|
|
}
|
|
func item(_ arguments: GroupStickersArguments, initialSize: NSSize) -> TableRowItem {
|
|
switch self {
|
|
case .section:
|
|
return GeneralRowItem(initialSize, height: 20, stableId: stableId, viewType: .separator)
|
|
case let .input(_, value, viewType):
|
|
return InputDataRowItem(initialSize, stableId: stableId, mode: .plain, error: nil, viewType: viewType, currentText: value, placeholder: nil, inputPlaceholder: "https://t.me/addstickers/", defaultText: "https://t.me/addstickers/", rightItem: .action(theme.icons.recentDismiss, .clearText), filter: { text in
|
|
var filter = NSCharacterSet.alphanumerics
|
|
filter.insert(charactersIn: "_/")
|
|
return text.trimmingCharacters(in: filter.inverted)
|
|
}, updated: { updatedText in
|
|
arguments.textChanged(value, updatedText)
|
|
}, pasteFilter: { value in
|
|
if let index = value.range(of: "t.me/addstickers/") {
|
|
return (true, String(value[index.upperBound...]))
|
|
}
|
|
return (false, value)
|
|
}, limit: 25 + 30)
|
|
|
|
case let .description(_, _, text, viewType):
|
|
let attr = NSMutableAttributedString()
|
|
_ = attr.append(string: text, color: theme.colors.grayText, font: .normal(.text))
|
|
|
|
attr.detectLinks(type: [.Mentions, .Hashtags], context: arguments.context, color: theme.colors.link, openInfo: { peerId, _, _, _ in
|
|
arguments.openChat(peerId)
|
|
})
|
|
return GeneralTextRowItem(initialSize, stableId: stableId, text: attr, viewType: viewType)
|
|
case let .status(_, status, viewType):
|
|
switch status {
|
|
case let .loaded(info, topItem, count):
|
|
return StickerSetTableRowItem(initialSize, context: arguments.context, stableId: stableId, info: info, topItem: topItem, itemCount: count, unread: false, editing: ItemListStickerPackItemEditing(editable: false, editing: false), enabled: true, control: .empty, viewType: viewType, action: {})
|
|
case .loading:
|
|
return LoadingTableItem(initialSize, height: 50, stableId: stableId, viewType: viewType)
|
|
case .failed:
|
|
return EmptyGroupstickerSearchRowItem(initialSize, height: 50, stableId: stableId, viewType: viewType)
|
|
}
|
|
case let .pack(_, _, info, topItem, count, selected, viewType):
|
|
return StickerSetTableRowItem(initialSize, context: arguments.context, stableId: stableId, info: info, topItem: topItem, itemCount: count, unread: false, editing: ItemListStickerPackItemEditing(editable: false, editing: false), enabled: true, control: selected ? .selected : .empty, viewType: viewType, action: {
|
|
if let topItem = topItem {
|
|
arguments.installStickerset((info, [topItem], count))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private func groupStickersEntries(state: GroupStickerSetControllerState, view: CombinedView, peerId: PeerId, specificPack: (StickerPackCollectionInfo, [ItemCollectionItem])?) -> [GroupStickersetEntry] {
|
|
var entries: [GroupStickersetEntry] = []
|
|
|
|
var sectionId:Int32 = 1
|
|
|
|
var descriptionId:Int32 = 1
|
|
|
|
entries.append(.section(sectionId))
|
|
sectionId += 1
|
|
|
|
let value: String
|
|
if let updatedText = state.updatedText {
|
|
value = updatedText
|
|
} else {
|
|
if let specificPack = specificPack {
|
|
value = specificPack.0.shortName
|
|
} else {
|
|
value = ""
|
|
}
|
|
}
|
|
|
|
|
|
var inputBlock: [GroupStickersetEntry] = []
|
|
func applyBlock(_ block:[GroupStickersetEntry]) {
|
|
var block = block
|
|
for (i, item) in block.enumerated() {
|
|
block[i] = item.withUpdatedViewType(bestGeneralViewType(block, for: i))
|
|
}
|
|
entries.append(contentsOf: block)
|
|
}
|
|
|
|
inputBlock.append(.input(sectionId, value: value, viewType: .singleItem))
|
|
|
|
if state.loading {
|
|
inputBlock.append(.status(sectionId, status: .loading, viewType: .singleItem))
|
|
} else {
|
|
if state.failed {
|
|
inputBlock.append(.status(sectionId, status: .failed, viewType: .singleItem))
|
|
} else if let loadedPack = state.loadedPack {
|
|
inputBlock.append(.status(sectionId, status: .loaded(loadedPack.0, loadedPack.1.first as? StickerPackItem, loadedPack.2), viewType: .singleItem))
|
|
} else {
|
|
if let specificPack = specificPack, !value.isEmpty {
|
|
inputBlock.append(.status(sectionId, status: .loaded(specificPack.0, specificPack.1.first as? StickerPackItem, Int32(specificPack.1.count)), viewType: .singleItem))
|
|
}
|
|
}
|
|
}
|
|
|
|
applyBlock(inputBlock)
|
|
|
|
entries.append(.description(sectionId, descriptionId, text: strings().groupStickersCreateDescription, viewType: .textBottomItem))
|
|
descriptionId += 1
|
|
|
|
|
|
entries.append(.section(sectionId))
|
|
sectionId += 1
|
|
|
|
|
|
|
|
entries.append(.description(sectionId, descriptionId, text: strings().groupStickersChooseHeader, viewType: .textTopItem))
|
|
descriptionId += 1
|
|
if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionInfosView {
|
|
if let packsEntries = stickerPacksView.entriesByNamespace[Namespaces.ItemCollection.CloudStickerPacks] {
|
|
var index: Int32 = 0
|
|
for (i, entry) in packsEntries.enumerated() {
|
|
if let info = entry.info as? StickerPackCollectionInfo {
|
|
var selected: Bool
|
|
if let loadedPack = state.loadedPack {
|
|
selected = info == loadedPack.0
|
|
} else {
|
|
selected = info == specificPack?.0
|
|
}
|
|
entries.append(.pack(sectionId, index, info, entry.firstItem as? StickerPackItem, info.count == 0 ? entry.count : info.count, selected, bestGeneralViewType(packsEntries, for: i)))
|
|
index += 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
entries.append(.section( sectionId))
|
|
sectionId += 1
|
|
|
|
|
|
return entries
|
|
}
|
|
|
|
private func prepareTransition(left:[AppearanceWrapperEntry<GroupStickersetEntry>], right: [AppearanceWrapperEntry<GroupStickersetEntry>], initialSize: NSSize, arguments: GroupStickersArguments) -> TableUpdateTransition {
|
|
let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in
|
|
return entry.entry.item(arguments, initialSize: initialSize)
|
|
}
|
|
return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true)
|
|
}
|
|
|
|
|
|
private struct GroupStickerSetControllerState: Equatable {
|
|
let loading: Bool
|
|
let updatedText:String?
|
|
let failed: Bool
|
|
let loadedPack:(StickerPackCollectionInfo, [ItemCollectionItem], Int32)?
|
|
init(loading: Bool = false, updatedText: String? = nil, failed: Bool = false, loadedPack:(StickerPackCollectionInfo, [ItemCollectionItem], Int32)? = nil) {
|
|
self.loading = loading
|
|
self.failed = failed
|
|
self.loadedPack = loadedPack
|
|
self.updatedText = updatedText
|
|
}
|
|
static func ==(lhs: GroupStickerSetControllerState, rhs: GroupStickerSetControllerState) -> Bool {
|
|
return lhs.loading == rhs.loading && lhs.updatedText == rhs.updatedText && lhs.failed == rhs.failed && lhs.loadedPack?.0 == rhs.loadedPack?.0
|
|
}
|
|
|
|
func withUpdatedText(_ updatedText:String?) -> GroupStickerSetControllerState {
|
|
return GroupStickerSetControllerState(loading: self.loading, updatedText: updatedText, failed: self.failed, loadedPack: self.loadedPack)
|
|
}
|
|
func withUpdatedLoading(_ loading:Bool) -> GroupStickerSetControllerState {
|
|
return GroupStickerSetControllerState(loading: loading, updatedText: self.updatedText, failed: self.failed, loadedPack: self.loadedPack)
|
|
}
|
|
func withUpdatedFailed(_ failed:Bool) -> GroupStickerSetControllerState {
|
|
return GroupStickerSetControllerState(loading: self.loading, updatedText: self.updatedText, failed: failed, loadedPack: self.loadedPack)
|
|
}
|
|
func withUpdatedLoadedPack(_ loadedPack:(StickerPackCollectionInfo, [ItemCollectionItem], Int32)?) -> GroupStickerSetControllerState {
|
|
return GroupStickerSetControllerState(loading: self.loading, updatedText: self.updatedText, failed: self.failed, loadedPack: loadedPack)
|
|
}
|
|
}
|
|
|
|
class GroupStickerSetController: TableViewController {
|
|
private let peerId: PeerId
|
|
private let disposable = MetaDisposable()
|
|
private var saveGroupStickerSet:(()->Void)? = nil
|
|
init(_ context: AccountContext, peerId:PeerId) {
|
|
self.peerId = peerId
|
|
super.init(context)
|
|
}
|
|
|
|
deinit {
|
|
disposable.dispose()
|
|
}
|
|
|
|
override func becomeFirstResponder() -> Bool? {
|
|
return true
|
|
}
|
|
|
|
override func firstResponder() -> NSResponder? {
|
|
var responder: NSResponder?
|
|
genericView.enumerateViews { view -> Bool in
|
|
if responder == nil, let firstResponder = view.firstResponder {
|
|
responder = firstResponder
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
return responder
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
let context = self.context
|
|
let peerId = self.peerId
|
|
|
|
let statePromise = ValuePromise(GroupStickerSetControllerState(), ignoreRepeated: true)
|
|
let stateValue = Atomic(value: GroupStickerSetControllerState())
|
|
let updateState: ((GroupStickerSetControllerState) -> GroupStickerSetControllerState) -> Void = { f in
|
|
statePromise.set(stateValue.modify { f($0) })
|
|
}
|
|
|
|
|
|
|
|
let actionsDisposable = DisposableSet()
|
|
|
|
let resolveDisposable = MetaDisposable()
|
|
actionsDisposable.add(resolveDisposable)
|
|
|
|
let arguments = GroupStickersArguments(context: context, textChanged: { current, updated in
|
|
updateState({$0.withUpdatedLoadedPack(nil).withUpdatedFailed(false).withUpdatedText(updated)})
|
|
if updated.isEmpty {
|
|
resolveDisposable.set(nil)
|
|
} else {
|
|
resolveDisposable.set((context.engine.stickers.loadedStickerPack(reference: .name(updated), forceActualized: false) |> deliverOnMainQueue).start(next: { result in
|
|
switch result {
|
|
case .fetching:
|
|
updateState({$0.withUpdatedLoadedPack(nil).withUpdatedLoading(true)})
|
|
case .none:
|
|
updateState({$0.withUpdatedLoadedPack(nil).withUpdatedLoading(false).withUpdatedFailed(true)})
|
|
case let .result(info, items, _):
|
|
updateState({$0.withUpdatedLoadedPack((info, items, Int32(items.count))).withUpdatedLoading(false).withUpdatedFailed(false)})
|
|
}
|
|
}))
|
|
}
|
|
}, installStickerset: { info in
|
|
updateState({$0.withUpdatedLoadedPack(info).withUpdatedText(info.0.shortName)})
|
|
}, openChat: { [weak self] peerId in
|
|
self?.navigationController?.push(ChatController(context: context, chatLocation: .peer(peerId)))
|
|
})
|
|
saveGroupStickerSet = { [weak self] in
|
|
if let strongSelf = self {
|
|
actionsDisposable.add(showModalProgress(signal: context.engine.peers.updateGroupSpecificStickerset(peerId: peerId, info: stateValue.modify{$0}.loadedPack?.0), for: context.window).start(next: { [weak strongSelf] _ in
|
|
strongSelf?.navigationController?.back()
|
|
}, error: { [weak strongSelf] _ in
|
|
strongSelf?.navigationController?.back()
|
|
}))
|
|
self?.doneButton?.isEnabled = false
|
|
}
|
|
}
|
|
|
|
let stickerPacks = Promise<CombinedView>()
|
|
stickerPacks.set(context.account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])]))
|
|
|
|
let featured = Promise<[FeaturedStickerPackItem]>()
|
|
featured.set(context.account.viewTracker.featuredStickerPacks())
|
|
|
|
let previousEntries:Atomic<[AppearanceWrapperEntry<GroupStickersetEntry>]> = Atomic(value: [])
|
|
let initialSize = self.atomicSize
|
|
|
|
let signal = combineLatest(queue: prepareQueue,statePromise.get(), stickerPacks.get(), context.engine.peers.peerSpecificStickerPack(peerId: peerId), appearanceSignal)
|
|
|> map { state, view, specificPack, appearance -> TableUpdateTransition in
|
|
let entries = groupStickersEntries(state: state, view: view, peerId: peerId, specificPack: specificPack.packInfo).map {AppearanceWrapperEntry(entry: $0, appearance: appearance)}
|
|
return prepareTransition(left: previousEntries.swap(entries), right: entries, initialSize: initialSize.modify({$0}), arguments: arguments)
|
|
} |> afterDisposed {
|
|
actionsDisposable.dispose()
|
|
} |> deliverOnMainQueue
|
|
|
|
self.disposable.set(signal.start(next: { [weak self] transition in
|
|
self?.genericView.merge(with: transition)
|
|
self?.readyOnce()
|
|
}))
|
|
|
|
|
|
|
|
actionsDisposable.add((statePromise.get() |> deliverOnMainQueue).start(next: { [weak self] state in
|
|
var enabled = !state.failed
|
|
if state.loadedPack != nil {
|
|
enabled = true
|
|
} else if let text = state.updatedText {
|
|
enabled = text.isEmpty
|
|
} else {
|
|
enabled = false
|
|
}
|
|
self?.doneButton?.isEnabled = enabled
|
|
}))
|
|
}
|
|
|
|
var doneButton:Control? {
|
|
return rightBarView
|
|
}
|
|
|
|
override func getRightBarViewOnce() -> BarView {
|
|
let button = TextButtonBarView(controller: self, text: strings().navigationDone)
|
|
|
|
button.set(handler: { [weak self] _ in
|
|
self?.saveGroupStickerSet?()
|
|
}, for: .Click)
|
|
|
|
return button
|
|
}
|
|
|
|
}
|