Files
TelegramSwift/Telegram-Mac/StickerPackPreviewModalController.swift
2024-01-30 16:32:12 +04:00

705 lines
26 KiB
Swift

//
// StickerPackPreviewModalController.swift
// Telegram
//
// Created by keepcoder on 27/02/2017.
// Copyright © 2017 Telegram. All rights reserved.
//
import Cocoa
import TGUIKit
import Postbox
import TelegramCore
import SwiftSignalKit
private enum State {
enum Source {
case stickers
case emoji
}
struct Collection {
let info: StickerPackCollectionInfo
let items: [StickerPackItem]
let installed: Bool
}
case loading(Source)
case loaded(source: Source, collections: [Collection])
}
private final class StickerPackArguments {
let context: AccountContext
let send:(TelegramMediaFile, NSView, Bool, Bool)->Void
let setEmoji:(TelegramMediaFile)->Void
let addpack:(State.Source, State.Collection, Bool)->Void
let addAll:(State.Source, [State.Collection], Bool)->Void
let share:(String)->Void
let close:()->Void
let previewPremium: (TelegramMediaFile, NSView)->Void
init(context: AccountContext, send:@escaping(Media, NSView, Bool, Bool)->Void, setEmoji:@escaping(TelegramMediaFile)->Void, addpack:@escaping(State.Source, State.Collection, Bool)->Void, addAll:@escaping(State.Source, [State.Collection], Bool)->Void, share:@escaping(String)->Void, close:@escaping()->Void, previewPremium: @escaping(TelegramMediaFile, NSView)->Void) {
self.context = context
self.send = send
self.setEmoji = setEmoji
self.addpack = addpack
self.addAll = addAll
self.share = share
self.close = close
self.previewPremium = previewPremium
}
}
extension FeaturedStickerPackItem : Equatable {
public static func == (lhs: FeaturedStickerPackItem, rhs: FeaturedStickerPackItem) -> Bool {
return lhs.info == rhs.info && lhs.unread == rhs.unread && lhs.topItems == rhs.topItems
}
}
enum StickerPackPreviewSource : Hashable {
case stickers(StickerPackReference)
case emoji(StickerPackReference)
var reference: StickerPackReference {
switch self {
case let .stickers(reference):
return reference
case let .emoji(reference):
return reference
}
}
}
private struct FeaturedEntry : TableItemListNodeEntry {
let item: FeaturedStickerPackItem
let index: Int
let installed: Bool
func item(_ arguments: StickerPanelArguments, initialSize: NSSize) -> TableRowItem {
return StickerPackPanelRowItem(initialSize, context: arguments.context, arguments: arguments, files: item.topItems.map { $0.file }, packInfo: .pack(item.info, installed: installed, featured: true), collectionId: .pack(item.info.id), canSend: false)
}
var stableId: AnyHashable {
return item.info.id
}
static func < (lhs: FeaturedEntry, rhs: FeaturedEntry) -> Bool {
return lhs.index < rhs.index
}
static func == (lhs: FeaturedEntry, rhs: FeaturedEntry) -> Bool {
return lhs.item == rhs.item && lhs.index == rhs.index && lhs.installed == rhs.installed
}
}
private class StickersModalView : View {
private let tableView:TableView = TableView(frame: NSZeroRect)
private let add:TextButton = TextButton()
private let shareView:ImageButton = ImageButton()
private let close: ImageButton = ImageButton()
private let headerTitle:TextView = TextView()
private let headerSeparatorView:View = View()
private let dismiss:ImageButton = ImageButton()
private var indicatorView:ProgressIndicator?
private let shadowView: ShadowView = ShadowView()
private var premiumView: StickerPremiumHolderView? = nil
required init(frame frameRect: NSRect) {
super.init(frame: frameRect)
backgroundColor = theme.colors.background
addSubview(tableView)
addSubview(headerTitle)
addSubview(shareView)
addSubview(close)
addSubview(headerSeparatorView)
addSubview(dismiss)
shadowView.shadowBackground = theme.colors.background
shadowView.setFrameSize(frame.width, 70)
addSubview(shadowView)
dismiss.set(image: theme.icons.stickerPackDelete, for: .Normal)
_ = dismiss.sizeToFit()
add.disableActions()
add.setFrameSize(170, 40)
add.layer?.cornerRadius = 20
add.set(color: theme.colors.underSelectedColor, for: .Normal)
add.set(font: .medium(.title), for: .Normal)
add.set(background: theme.colors.accent, for: .Normal)
add.set(background: theme.colors.accent, for: .Hover)
add.set(background: theme.colors.accent, for: .Highlight)
addSubview(add)
add.scaleOnClick = true
headerTitle.backgroundColor = theme.colors.background
headerSeparatorView.backgroundColor = theme.colors.border
shareView.set(image: theme.icons.stickersShare, for: .Normal)
close.set(image: theme.icons.stickerPackClose, for: .Normal)
_ = shareView.sizeToFit()
_ = close.sizeToFit()
dismiss.scaleOnClick = true
close.scaleOnClick = true
}
func previewPremium(_ file: TelegramMediaFile, context: AccountContext, view: NSView, animated: Bool) {
let current: StickerPremiumHolderView
if let view = premiumView {
current = view
} else {
current = StickerPremiumHolderView(frame: bounds)
self.premiumView = current
addSubview(current)
if animated {
current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2)
}
}
current.set(file: file, context: context, callback: { [weak self] in
showModal(with: PremiumBoardingController(context: context, source: .premium_stickers), for: context.window)
self?.closePremium()
})
current.close = { [weak self] in
self?.closePremium()
}
}
var isPremium: Bool {
return self.premiumView != nil
}
func closePremium() {
if let view = premiumView {
performSubviewRemoval(view, animated: true)
self.premiumView = nil
}
}
func layout(with state: State, source controllerSource: StickerPackPreviewModalController.Source, installed: Set<StickerPackPreviewSource>, arguments: StickerPackArguments) -> Void {
switch state {
case .loading:
dismiss.isHidden = true
shareView.isHidden = true
if self.indicatorView == nil {
self.indicatorView = ProgressIndicator(frame: NSMakeRect(0, 0, 30, 30))
addSubview(self.indicatorView!)
}
self.indicatorView?.center()
add.isHidden = true
shadowView.isHidden = true
case let .loaded(source, collections):
if let indicatorView = self.indicatorView {
performSubviewRemoval(indicatorView, animated: true)
self.indicatorView = nil
}
let isInstalled:(State.Collection)->Bool = { collection in
if collection.installed {
return true
}
switch source {
case .emoji:
return installed.contains(.emoji(.name(collection.info.shortName)))
case .stickers:
return installed.contains(.stickers(.name(collection.info.shortName)))
}
}
let allInstalled: Bool
let notInstalledCount = collections.filter({ !isInstalled($0) }).count
switch controllerSource {
case .install:
dismiss.isHidden = collections.count > 1 || !isInstalled(collections[0])
shareView.isHidden = false
allInstalled = !collections.contains(where: { !isInstalled($0) })
add.isHidden = allInstalled
shadowView.isHidden = allInstalled
if !allInstalled {
switch source {
case .emoji:
if collections.count == 1 {
add.set(text: strings().emojiPackAddCountable(collections[0].items.count).uppercased(), for: .Normal)
} else {
add.set(text: strings().emojiPackSetsAddCountable(notInstalledCount).uppercased(), for: .Normal)
}
case .stickers:
if collections.count == 1 {
add.set(text: strings().stickerPackAdd1Countable(collections[0].items.count).uppercased(), for: .Normal)
} else {
add.set(text: strings().stickerPackSetsAdd1Countable(notInstalledCount).uppercased(), for: .Normal)
}
}
_ = add.sizeToFit(NSMakeSize(20, 0), NSMakeSize(frame.width - 40, 40), thatFit: false)
}
default:
dismiss.isHidden = true
shareView.isHidden = true
add.isHidden = false
shadowView.isHidden = false
allInstalled = false
switch controllerSource {
case .installGroupEmojiPack:
add.set(text: "Set as Group Emoji Pack", for: .Normal)
case .removeGroupEmojiPack:
add.set(text: "Remove Group Emoji Pack", for: .Normal)
default:
break
}
_ = add.sizeToFit(NSMakeSize(20, 0), NSMakeSize(frame.width - 40, 40), thatFit: false)
}
let attr = NSMutableAttributedString()
if collections.count == 1 {
let collection = collections[0]
_ = attr.append(string: collection.info.title, color: theme.colors.text, font: .medium(16.0))
} else {
switch source {
case .emoji:
_ = attr.append(string: strings().stickerPackEmoji, color: theme.colors.text, font: .medium(16.0))
case .stickers:
_ = attr.append(string: strings().stickerPackStickers, color: theme.colors.text, font: .medium(16.0))
}
}
attr.detectLinks(type: [.Mentions], context: arguments.context, color: theme.colors.accent, openInfo: { (peerId, _, _, _) in
_ = (arguments.context.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { peer in
arguments.close()
if peer.isUser || peer.isBot {
let navigation = arguments.context.bindings.rootNavigation()
PeerInfoController.push(navigation: navigation, context: arguments.context, peerId: peerId)
} else {
arguments.context.bindings.rootNavigation().push(ChatAdditionController(context: arguments.context, chatLocation: .peer(peer.id)))
}
})
})
let layout = TextViewLayout(attr, maximumNumberOfLines: 2, alignment: .center)
layout.interactions = globalLinkExecutor
layout.measure(width: frame.width - 160)
headerTitle.update(layout)
let context = arguments.context
let stickerArguments = StickerPanelArguments(context: arguments.context, sendMedia: { media, view, silent, schedule, _ in
if let media = media as? TelegramMediaFile {
if media.isPremiumSticker && !context.isPremium {
arguments.previewPremium(media, view)
} else {
arguments.send(media, view, silent, schedule)
}
}
}, showPack: { _ in
}, addPack: { _ in
}, navigate: { _ in
}, clearRecent: {
}, removePack: { _ in
}, closeInlineFeatured: { _ in
}, openFeatured: { _ in
}, selectEmojiCategory: { _ in
}, mode: .common)
let size = frame.size
let makeItems:(State.Collection)->[TableRowItem] = { collection in
switch source {
case .emoji:
let array = collection.items.chunks(32)
var tableItems: [TableRowItem] = []
for (i, items) in array.enumerated() {
tableItems.append(EmojiesSectionRowItem(size, stableId: arc4random64(), context: arguments.context, revealed: true, installed: isInstalled(collection), info: i != 0 ? nil : collection.info, items: items, mode: .preview, callback: { item, _, _, _ in
arguments.setEmoji(item.file)
}, installPack: { _, _ in
arguments.addpack(source, collection, false)
}))
}
return tableItems
case .stickers:
let files = collection.items.map { item -> TelegramMediaFile in
return item.file
}
return [StickerPackPanelRowItem(size, context: arguments.context, arguments: stickerArguments, files: files, packInfo: .emojiRelated, collectionId: .pack(collection.info.id), canSend: arguments.context.bindings.rootNavigation().controller is ChatController, isPreview: true)]
}
}
tableView.beginTableUpdates()
tableView.removeAll()
_ = tableView.addItem(item: GeneralRowItem(frame.size, height: 10, stableId: arc4random64()))
for collection in collections {
let items = makeItems(collection)
for item in items {
_ = item.makeSize(frame.width)
_ = tableView.addItem(item: item, animation: .none)
}
}
if !allInstalled {
_ = tableView.addItem(item: GeneralRowItem(frame.size, height: 70, stableId: arc4random64()))
}
tableView.endTableUpdates()
self.needsLayout = true
shareView.set(handler: { _ in
let shareText: String = collections.reduce("", { current, value in
let text: String
switch source {
case .emoji:
text = "https://t.me/addemoji/\(value.info.shortName)"
case .stickers:
text = "https://t.me/addstickers/\(value.info.shortName)"
}
return current + text + "\n"
})
arguments.share(shareText.trimmed)
}, for: .SingleClick)
//
add.removeAllHandlers()
dismiss.removeAllHandlers()
close.removeAllHandlers()
//
func action(_ control:Control) {
if collections.count == 1 {
arguments.addpack(source, collections[0], true)
} else {
arguments.addAll(source, collections, true)
}
}
add.set(handler: action, for: .SingleClick)
dismiss.set(handler: action, for: .SingleClick)
close.set(handler: { _ in
arguments.close()
}, for: .Click)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layout() {
super.layout()
let headerHeight:CGFloat = 50
tableView.frame = NSMakeRect(0, headerHeight, frame.width, frame.height - headerHeight)
headerTitle.centerX(y : floorToScreenPixels(backingScaleFactor, (headerHeight - headerTitle.frame.height)/2) + 1)
headerSeparatorView.frame = NSMakeRect(0, headerHeight - .borderSize, frame.width, .borderSize)
shareView.setFrameOrigin(frame.width - close.frame.width - 12, floorToScreenPixels(backingScaleFactor, (headerHeight - shareView.frame.height)/2))
close.setFrameOrigin(12, floorToScreenPixels(backingScaleFactor, (headerHeight - shareView.frame.height)/2))
add.centerX(y: frame.height - add.frame.height - 15)
dismiss.setFrameOrigin(NSMakePoint(shareView.frame.minX - dismiss.frame.width - 15, floorToScreenPixels(backingScaleFactor, (headerHeight - shareView.frame.height)/2)))
shadowView.setFrameOrigin(0, frame.height - shadowView.frame.height)
premiumView?.frame = bounds
}
}
private final class SetPreviewController: TableViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
class StickerPackPreviewModalController: ModalViewController {
private let context:AccountContext
private let peerId:PeerId?
private let references: [StickerPackPreviewSource]
private let disposable: MetaDisposable = MetaDisposable()
private var arguments:StickerPackArguments!
private var onAdd:(()->Void)? = nil
private let source: Source
private let installedValue = ValuePromise<Set<StickerPackPreviewSource>>(Set(), ignoreRepeated: true)
private var installed: Set<StickerPackPreviewSource> = Set() {
didSet {
installedValue.set(installed)
}
}
enum Source {
case install
case installGroupEmojiPack
case removeGroupEmojiPack
}
init(_ context: AccountContext, peerId:PeerId?, references: [StickerPackPreviewSource], onAdd:(()->Void)? = nil, source controllerSource: Source = .install) {
self.context = context
self.peerId = peerId
self.references = references
self.onAdd = onAdd
self.source = controllerSource
super.init(frame: NSMakeRect(0, 0, 350, 400))
bar = .init(height: 0)
arguments = StickerPackArguments(context: context, send: { [weak self] media, view, silent, schedule in
let interactions = (context.bindings.rootNavigation().controller as? ChatController)?.chatInteraction
if let interactions = interactions, let media = media as? TelegramMediaFile, media.maskData == nil {
if let slowMode = interactions.presentation.slowMode, slowMode.hasLocked {
showSlowModeTimeoutTooltip(slowMode, for: view)
} else {
interactions.sendAppFile(media, silent, nil, schedule, nil)
self?.close()
}
}
}, setEmoji: { [weak self] file in
let interactions = (context.bindings.rootNavigation().controller as? ChatController)?.chatInteraction
guard let interactions = interactions else {
return
}
if context.isPremium {
if interactions.presentation.state == .normal {
_ = interactions.appendText(.makeAnimated(file, text: file.customEmojiText ?? ""), selectedRange: nil)
interactions.showEmojiUseTooltip()
self?.close()
}
} else {
showModalText(for: context.window, text: strings().emojiPackPremiumAlert, callback: { _ in
showModal(with: PremiumBoardingController(context: context, source: .premium_emoji), for: context.window)
})
}
}, addpack: { [weak self] source, collection, close in
switch controllerSource {
case .install:
let title: String
let text: String
if !collection.installed {
_ = context.engine.stickers.addStickerPackInteractively(info: collection.info, items: collection.items).start()
switch source {
case .stickers:
title = strings().stickerPackAddedTitle
text = strings().stickerPackAddedInfo(collection.info.title)
case .emoji:
title = strings().emojiPackAddedTitle
text = strings().emojiPackAddedInfo(collection.info.title)
}
} else {
switch source {
case .stickers:
title = strings().stickerPackRemovedTitle
text = strings().stickerPackRemovedInfo(collection.info.title)
case .emoji:
title = strings().emojiPackRemovedTitle
text = strings().emojiPackRemovedInfo(collection.info.title)
}
_ = context.engine.stickers.removeStickerPackInteractively(id: collection.info.id, option: .archive).start()
}
showModalText(for: context.window, text: text, title: title)
if close {
self?.close()
self?.disposable.dispose()
} else {
switch source {
case .emoji:
self?.installed.insert(.emoji(.name(collection.info.shortName)))
case .stickers:
self?.installed.insert(.emoji(.name(collection.info.shortName)))
}
}
self?.onAdd?()
default:
self?.close()
self?.disposable.dispose()
self?.onAdd?()
}
}, addAll: { [weak self] source, collections, close in
let signals = collections.map {
context.engine.stickers.addStickerPackInteractively(info: $0.info, items: $0.items)
}
_ = combineLatest(signals).start()
if close {
self?.close()
self?.disposable.dispose()
} else {
for collection in collections {
switch source {
case .emoji:
self?.installed.insert(.emoji(.name(collection.info.shortName)))
case .stickers:
self?.installed.insert(.emoji(.name(collection.info.shortName)))
}
}
}
self?.onAdd?()
}, share: { [weak self] link in
self?.close()
showModal(with: ShareModalController(ShareLinkObject(context, link: link)), for: context.window)
}, close: { [weak self] in
self?.close()
}, previewPremium: { [weak self] file, view in
self?.genericView.previewPremium(file, context: context, view: view, animated: true)
})
}
fileprivate var genericView:StickersModalView {
return self.view as! StickersModalView
}
override func viewClass() -> AnyClass {
return StickersModalView.self
}
override var dynamicSize: Bool {
return true
}
override func measure(size: NSSize) {
// self.modal?.resize(with:NSMakeSize(genericView.frame.width, min(size.height - 70, genericView.listHeight)), animated: false)
}
override func viewDidLoad() {
super.viewDidLoad()
func namespaceForMode(_ mode: StickerPackPreviewSource) -> ItemCollectionId.Namespace {
switch mode {
case .stickers:
return Namespaces.ItemCollection.CloudStickerPacks
case .emoji:
return Namespaces.ItemCollection.CloudEmojiPacks
}
}
let namespaces = self.references.map { namespaceForMode($0) }
let namespace = namespaces[0]
let references = self.references
let context = self.context
let signal = combineLatest(references.map {
context.engine.stickers.loadedStickerPack(reference: $0.reference, forceActualized: true)
}) |> deliverOnMainQueue
disposable.set(combineLatest(signal, installedValue.get()).start(next: { [weak self] result, installed in
guard let `self` = self else {return}
let isEmpty = result.filter { value in
switch value {
case .none:
return true
default:
return false
}
}.count == result.count
let isLoaded = result.filter { value in
switch value {
case .fetching:
return true
default:
return false
}
}.isEmpty
let state: State
let source: State.Source
if namespace == Namespaces.ItemCollection.CloudEmojiPacks {
source = .emoji
} else {
source = .stickers
}
let collections:[State.Collection] = result.compactMap { value in
switch value {
case let .result(info, items, installed):
return .init(info: info, items: items, installed: installed)
default:
return nil
}
}
state = isLoaded ? .loaded(source: source, collections: collections) : .loading(source)
if isEmpty {
alert(for: context.window, info: strings().stickerSetDontExist)
self.close()
} else {
self.genericView.layout(with: state, source: self.source, installed: installed, arguments: self.arguments)
self.readyOnce()
}
}))
}
override func becomeFirstResponder() -> Bool? {
return nil
}
override var canBecomeResponder: Bool {
return false
}
override func escapeKeyAction() -> KeyHandlerResult {
if self.genericView.isPremium {
self.genericView.closePremium()
return .invoked
} else {
return super.escapeKeyAction()
}
}
deinit {
disposable.dispose()
}
}