mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-30 09:21:28 +00:00
1523 lines
76 KiB
Swift
1523 lines
76 KiB
Swift
import AsyncDisplayKit
|
|
import UIKit
|
|
import Display
|
|
import ComponentFlow
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramPresentationData
|
|
import PresentationDataUtils
|
|
import AccountContext
|
|
import ContextUI
|
|
import PhotoResources
|
|
import TelegramUIPreferences
|
|
import TelegramStringFormatting
|
|
import ItemListPeerItem
|
|
import ItemListPeerActionItem
|
|
import MergeLists
|
|
import ItemListUI
|
|
import ChatControllerInteraction
|
|
import MultilineTextComponent
|
|
import BalancedTextComponent
|
|
import Markdown
|
|
import PeerInfoPaneNode
|
|
import GiftItemComponent
|
|
import PlainButtonComponent
|
|
import GiftViewScreen
|
|
import ButtonComponent
|
|
import UndoUI
|
|
import CheckComponent
|
|
import LottieComponent
|
|
import ContextUI
|
|
import TabSelectorComponent
|
|
import BundleIconComponent
|
|
import EmojiTextAttachmentView
|
|
import TextFormat
|
|
import PromptUI
|
|
|
|
public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate {
|
|
public enum GiftCollection: Equatable {
|
|
case all
|
|
case collection(Int32)
|
|
case create
|
|
|
|
init(rawValue: Int32) {
|
|
switch rawValue {
|
|
case 0:
|
|
self = .all
|
|
case -1:
|
|
self = .create
|
|
default:
|
|
self = .collection(rawValue)
|
|
}
|
|
}
|
|
|
|
public var rawValue: Int32 {
|
|
switch self {
|
|
case .all:
|
|
return 0
|
|
case .create:
|
|
return -1
|
|
case let .collection(id):
|
|
return id
|
|
}
|
|
}
|
|
}
|
|
|
|
private let context: AccountContext
|
|
private let peerId: PeerId
|
|
private let profileGiftsCollections: ProfileGiftsCollectionsContext
|
|
private let profileGifts: ProfileGiftsContext
|
|
private let canManage: Bool
|
|
private let canGift: Bool
|
|
private var resultsAreEmpty = false
|
|
|
|
private let chatControllerInteraction: ChatControllerInteraction
|
|
|
|
public weak var parentController: ViewController? {
|
|
didSet {
|
|
self.giftsListView.parentController = self.parentController
|
|
}
|
|
}
|
|
|
|
private let backgroundNode: ASDisplayNode
|
|
private let scrollNode: ASScrollNode
|
|
private var giftsListView: GiftsListView
|
|
|
|
private let tabSelector = ComponentView<Empty>()
|
|
public private(set) var currentCollection: GiftCollection = .all
|
|
|
|
private var footerText: ComponentView<Empty>?
|
|
private var panelBackground: NavigationBackgroundNode?
|
|
private var panelSeparator: ASDisplayNode?
|
|
private var panelButton: ComponentView<Empty>?
|
|
private var panelCheck: ComponentView<Empty>?
|
|
|
|
private let emptyResultsClippingView = UIView()
|
|
private let emptyResultsAnimation = ComponentView<Empty>()
|
|
private let emptyResultsTitle = ComponentView<Empty>()
|
|
private let emptyResultsAction = ComponentView<Empty>()
|
|
|
|
private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData)?
|
|
|
|
private var theme: PresentationTheme?
|
|
private let presentationDataPromise = Promise<PresentationData>()
|
|
|
|
private var collectionsDisposable: Disposable?
|
|
private var collections: [StarGiftCollection]?
|
|
private var reorderedCollectionIds: [Int32]?
|
|
private var isReordering = false
|
|
|
|
private let ready = Promise<Bool>()
|
|
private var didSetReady: Bool = false
|
|
public var isReady: Signal<Bool, NoError> {
|
|
return self.ready.get()
|
|
}
|
|
|
|
private let statusPromise = Promise<PeerInfoStatusData?>(nil)
|
|
public var status: Signal<PeerInfoStatusData?, NoError> {
|
|
self.statusPromise.get()
|
|
}
|
|
|
|
public var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
|
public var tabBarOffset: CGFloat {
|
|
return 0.0
|
|
}
|
|
|
|
public var giftsContext: ProfileGiftsContext {
|
|
return self.giftsListView.profileGifts
|
|
}
|
|
|
|
private let collectionsMaxCount: Int
|
|
|
|
public init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, profileGiftsCollections: ProfileGiftsCollectionsContext, profileGifts: ProfileGiftsContext, canManage: Bool, canGift: Bool) {
|
|
self.context = context
|
|
self.peerId = peerId
|
|
self.chatControllerInteraction = chatControllerInteraction
|
|
self.profileGiftsCollections = profileGiftsCollections
|
|
self.profileGifts = profileGifts
|
|
self.canManage = canManage
|
|
self.canGift = canGift
|
|
|
|
if let value = context.currentAppConfiguration.with({ $0 }).data?["stargifts_collections_limit"] as? Double {
|
|
self.collectionsMaxCount = Int(value)
|
|
} else {
|
|
self.collectionsMaxCount = 6
|
|
}
|
|
|
|
self.backgroundNode = ASDisplayNode()
|
|
self.scrollNode = ASScrollNode()
|
|
self.giftsListView = GiftsListView(context: context, peerId: peerId, profileGifts: profileGifts, giftsCollections: profileGiftsCollections, canSelect: false)
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.backgroundNode)
|
|
self.addSubnode(self.scrollNode)
|
|
|
|
self.statusPromise.set(self.giftsListView.status)
|
|
self.ready.set(self.giftsListView.isReady)
|
|
|
|
self.giftsListView.contextAction = { [weak self] gift, view, gesture in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.contextAction(gift: gift, view: view, gesture: gesture)
|
|
}
|
|
|
|
self.collectionsDisposable = (profileGiftsCollections.state
|
|
|> deliverOnMainQueue).start(next: { [weak self] state in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.collections = state.collections
|
|
self.updateScrolling(transition: .easeInOut(duration: 0.2))
|
|
})
|
|
}
|
|
|
|
deinit {
|
|
self.collectionsDisposable?.dispose()
|
|
}
|
|
|
|
public override func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
|
self.scrollNode.view.delegate = self
|
|
|
|
self.scrollNode.view.insertSubview(self.giftsListView, at: 0)
|
|
}
|
|
|
|
private func item(at point: CGPoint) -> (AnyHashable, ComponentView<Empty>)? {
|
|
return self.giftsListView.item(at: self.giftsListView.convert(point, from: self.view))
|
|
}
|
|
|
|
public func createCollection(gifts: [ProfileGiftsContext.State.StarGift] = []) {
|
|
guard let params = self.currentParams else {
|
|
return
|
|
}
|
|
if let collections = self.collections, collections.count >= self.collectionsMaxCount {
|
|
let alertController = textAlertController(context: self.context, title: params.presentationData.strings.PeerInfo_Gifts_CollectionLimitReached_Title, text: params.presentationData.strings.PeerInfo_Gifts_CollectionLimitReached_Text, actions: [TextAlertAction(type: .defaultAction, title: params.presentationData.strings.Common_OK, action: {})])
|
|
self.parentController?.present(alertController, in: .window(.root))
|
|
return
|
|
}
|
|
|
|
let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Title, titleFont: .bold, subtitle: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Text, value: "", placeholder: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Placeholder, characterLimit: 12, displayCharacterLimit: true, apply: { [weak self] value in
|
|
guard let self, let value else {
|
|
return
|
|
}
|
|
let _ = self.profileGiftsCollections.createCollection(title: value, starGifts: gifts).start(next: { [weak self] collection in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if let collection {
|
|
self.setCurrentCollection(collection: .collection(collection.id))
|
|
}
|
|
})
|
|
})
|
|
self.parentController?.present(promptController, in: .window(.root))
|
|
}
|
|
|
|
public func deleteCollection(id: Int32) {
|
|
guard let params = self.currentParams else {
|
|
return
|
|
}
|
|
let actionSheet = ActionSheetController(presentationData: params.presentationData)
|
|
actionSheet.setItemGroups([
|
|
ActionSheetItemGroup(items: [
|
|
ActionSheetTextItem(title: params.presentationData.strings.PeerInfo_Gifts_RemoveCollectionConfirmation),
|
|
ActionSheetButtonItem(title: params.presentationData.strings.PeerInfo_Gifts_RemoveCollectionAction, color: .destructive, action: { [weak self, weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
|
|
self?.setCurrentCollection(collection: .all)
|
|
let _ = self?.profileGiftsCollections.deleteCollection(id: id).start()
|
|
})
|
|
]),
|
|
ActionSheetItemGroup(items: [
|
|
ActionSheetButtonItem(title: params.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
})
|
|
])
|
|
])
|
|
self.parentController?.present(actionSheet, in: .window(.root))
|
|
}
|
|
|
|
public func addGiftsToCollection(id: Int32) {
|
|
var collectionGiftsMaxCount: Int32 = 1000
|
|
if let value = self.context.currentAppConfiguration.with({ $0 }).data?["stargifts_collection_gifts_limit"] as? Double {
|
|
collectionGiftsMaxCount = Int32(value)
|
|
}
|
|
var remainingCount = collectionGiftsMaxCount
|
|
if let currentCount = self.giftsListView.profileGifts.currentState?.count {
|
|
remainingCount = max(0, collectionGiftsMaxCount - currentCount)
|
|
}
|
|
let screen = AddGiftsScreen(context: self.context, peerId: self.peerId, collectionId: id, remainingCount: remainingCount, completion: { [weak self] gifts in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let _ = self.profileGiftsCollections.addGifts(id: id, gifts: gifts).start()
|
|
})
|
|
self.parentController?.push(screen)
|
|
}
|
|
|
|
public func renameCollection(id: Int32) {
|
|
guard let params = self.currentParams, let collection = self.collections?.first(where: { $0.id == id }) else {
|
|
return
|
|
}
|
|
|
|
let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: params.presentationData.strings.PeerInfo_Gifts_RenameCollection_Title, titleFont: .bold, value: collection.title, placeholder: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Placeholder, characterLimit: 12, displayCharacterLimit: true, apply: { [weak self] value in
|
|
guard let self, let value else {
|
|
return
|
|
}
|
|
let _ = self.profileGiftsCollections.renameCollection(id: id, title: value).start()
|
|
})
|
|
self.parentController?.present(promptController, in: .window(.root))
|
|
}
|
|
|
|
public func beginReordering() {
|
|
self.giftsListView.beginReordering()
|
|
}
|
|
|
|
public func endReordering() {
|
|
self.giftsListView.endReordering()
|
|
}
|
|
|
|
public func updateIsReordering(isReordering: Bool, animated: Bool) {
|
|
if self.isReordering != isReordering {
|
|
self.isReordering = isReordering
|
|
|
|
if let collections = self.collections {
|
|
if isReordering {
|
|
var collectionIds: [Int32] = []
|
|
for collection in collections {
|
|
collectionIds.append(collection.id)
|
|
}
|
|
self.reorderedCollectionIds = collectionIds
|
|
} else if let reorderedCollectionIds = self.reorderedCollectionIds {
|
|
let _ = self.profileGiftsCollections.reorderCollections(order: reorderedCollectionIds).start()
|
|
Queue.mainQueue().after(1.0, {
|
|
self.reorderedCollectionIds = nil
|
|
})
|
|
}
|
|
}
|
|
|
|
self.giftsListView.updateIsReordering(isReordering: isReordering, animated: animated)
|
|
self.updateScrolling(transition: .easeInOut(duration: 0.2))
|
|
}
|
|
}
|
|
|
|
public func ensureMessageIsVisible(id: MessageId) {
|
|
}
|
|
|
|
public func scrollToTop() -> Bool {
|
|
self.scrollNode.view.setContentOffset(.zero, animated: true)
|
|
return true
|
|
}
|
|
|
|
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
self.updateScrolling(interactive: true, transition: .immediate)
|
|
}
|
|
|
|
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
cancelContextGestures(view: scrollView)
|
|
}
|
|
|
|
private func displayUnpinScreen(gift: ProfileGiftsContext.State.StarGift, completion: (() -> Void)? = nil) {
|
|
guard let pinnedGifts = self.profileGifts.currentState?.gifts.filter({ $0.pinnedToTop }), let presentationData = self.currentParams?.presentationData else {
|
|
return
|
|
}
|
|
let controller = GiftUnpinScreen(
|
|
context: self.context,
|
|
gift: gift,
|
|
pinnedGifts: pinnedGifts,
|
|
completion: { [weak self] unpinnedReference in
|
|
guard let self else {
|
|
return
|
|
}
|
|
completion?()
|
|
|
|
var replacingTitle = ""
|
|
for gift in pinnedGifts {
|
|
if gift.reference == unpinnedReference, case let .unique(uniqueGift) = gift.gift {
|
|
replacingTitle = "\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, presentationData.dateTimeFormat.groupingSeparator))"
|
|
}
|
|
}
|
|
|
|
var updatedPinnedGifts = self.giftsListView.pinnedReferences
|
|
if let index = updatedPinnedGifts.firstIndex(of: unpinnedReference), let reference = gift.reference {
|
|
updatedPinnedGifts[index] = reference
|
|
}
|
|
self.profileGifts.updatePinnedToTopStarGifts(references: updatedPinnedGifts)
|
|
|
|
var title = ""
|
|
if case let .unique(uniqueGift) = gift.gift {
|
|
title = "\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, presentationData.dateTimeFormat.groupingSeparator))"
|
|
}
|
|
|
|
let _ = self.scrollToTop()
|
|
Queue.mainQueue().after(0.35) {
|
|
let toastTitle = presentationData.strings.PeerInfo_Gifts_ToastPinned_TitleNew(title).string
|
|
let toastText = presentationData.strings.PeerInfo_Gifts_ToastPinned_ReplacingText(replacingTitle).string
|
|
self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_toastpin", scale: 0.06, colors: [:], title: toastTitle, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
|
}
|
|
}
|
|
)
|
|
self.parentController?.push(controller)
|
|
}
|
|
|
|
func setCurrentCollection(collection: GiftCollection) {
|
|
guard self.currentCollection != collection else {
|
|
return
|
|
}
|
|
var animateRight = false
|
|
if case let .collection(currentId) = self.currentCollection {
|
|
if case let .collection(nextId) = collection {
|
|
if let currentIndex = self.collections?.firstIndex(where: { $0.id == currentId }), let nextIndex = self.collections?.firstIndex(where: { $0.id == nextId }) {
|
|
animateRight = nextIndex > currentIndex
|
|
}
|
|
}
|
|
} else {
|
|
animateRight = true
|
|
}
|
|
|
|
let previousGiftsListView = self.giftsListView
|
|
|
|
let profileGifts: ProfileGiftsContext
|
|
switch collection {
|
|
case let .collection(id):
|
|
profileGifts = self.profileGiftsCollections.giftsContextForCollection(id: id)
|
|
default:
|
|
profileGifts = self.profileGifts
|
|
}
|
|
if case .ready = profileGifts.currentState?.dataState {
|
|
profileGifts.reload()
|
|
}
|
|
|
|
self.giftsListView = GiftsListView(context: self.context, peerId: self.peerId, profileGifts: profileGifts, giftsCollections: self.profileGiftsCollections, canSelect: false)
|
|
self.giftsListView.addToCollection = { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if case let .collection(id) = collection {
|
|
self.addGiftsToCollection(id: id)
|
|
}
|
|
}
|
|
self.giftsListView.onContentUpdated = { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if case .collection = collection {
|
|
self.resultsAreEmpty = self.giftsListView.resultsAreEmpty
|
|
} else {
|
|
self.resultsAreEmpty = false
|
|
}
|
|
if let params = self.currentParams {
|
|
self.update(size: params.size, topInset: params.topInset, sideInset: params.sideInset, bottomInset: params.bottomInset, deviceMetrics: params.deviceMetrics, visibleHeight: params.visibleHeight, isScrollingLockedAtTop: params.isScrollingLockedAtTop, expandProgress: params.expandProgress, navigationHeight: params.navigationHeight, presentationData: params.presentationData, synchronous: true, transition: .immediate)
|
|
}
|
|
}
|
|
self.giftsListView.parentController = self.parentController
|
|
self.giftsListView.contextAction = { [weak self] gift, view, gesture in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.contextAction(gift: gift, view: view, gesture: gesture)
|
|
}
|
|
self.giftsListView.frame = previousGiftsListView.frame
|
|
|
|
self.scrollNode.view.insertSubview(self.giftsListView, aboveSubview: previousGiftsListView)
|
|
|
|
let multiplier = animateRight ? 1.0 : -1.0
|
|
|
|
previousGiftsListView.layer.animatePosition(from: .zero, to: CGPoint(x: previousGiftsListView.frame.width * multiplier * -1.0, y: 0.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in
|
|
previousGiftsListView.removeFromSuperview()
|
|
})
|
|
self.giftsListView.layer.animatePosition(from: CGPoint(x: previousGiftsListView.frame.width * multiplier, y: 0.0), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
|
|
|
self.currentCollection = collection
|
|
self.updateScrolling(transition: .spring(duration: 0.25))
|
|
|
|
if let params = self.currentParams {
|
|
let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -10.0)
|
|
let _ = self.giftsListView.update(size: params.size, sideInset: params.sideInset, bottomInset: params.bottomInset, deviceMetrics: params.deviceMetrics, visibleHeight: params.visibleHeight, isScrollingLockedAtTop: params.isScrollingLockedAtTop, expandProgress: params.expandProgress, presentationData: params.presentationData, synchronous: true, visibleBounds: visibleBounds, transition: .immediate)
|
|
}
|
|
}
|
|
|
|
func openCollectionContextMenu(id: Int32, sourceNode: ASDisplayNode, gesture: ContextGesture?) {
|
|
guard let params = self.currentParams, let sourceNode = sourceNode as? ContextExtractedContentContainingNode else {
|
|
return
|
|
}
|
|
|
|
var items: [ContextMenuItem] = []
|
|
|
|
items.append(.action(ContextMenuActionItem(text: params.presentationData.strings.PeerInfo_Gifts_AddGifts, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/Gifts/AddGift"), color: theme.actionSheet.primaryTextColor)
|
|
}, action: { [weak self] _, f in
|
|
guard let self else {
|
|
return
|
|
}
|
|
f(.default)
|
|
|
|
self.setCurrentCollection(collection: .collection(id))
|
|
self.addGiftsToCollection(id: id)
|
|
})))
|
|
|
|
items.append(.action(ContextMenuActionItem(text: params.presentationData.strings.PeerInfo_Gifts_RenameCollection, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor)
|
|
}, action: { [weak self] _, f in
|
|
guard let self else {
|
|
return
|
|
}
|
|
f(.default)
|
|
|
|
self.renameCollection(id: id)
|
|
})))
|
|
|
|
items.append(.action(ContextMenuActionItem(text: params.presentationData.strings.PeerInfo_Gifts_ShareCollection, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.actionSheet.primaryTextColor)
|
|
}, action: { [weak self] _, f in
|
|
guard let self else {
|
|
return
|
|
}
|
|
f(.default)
|
|
|
|
//TODO:release
|
|
let _ = self
|
|
})))
|
|
|
|
items.append(.action(ContextMenuActionItem(text: params.presentationData.strings.PeerInfo_Gifts_Reorder, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.actionSheet.primaryTextColor)
|
|
}, action: { [weak self] c, f in
|
|
c?.dismiss(completion: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.beginReordering()
|
|
})
|
|
})))
|
|
|
|
items.append(.action(ContextMenuActionItem(text: params.presentationData.strings.PeerInfo_Gifts_DeleteCollection, textColor: .destructive, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
|
}, action: { [weak self] _, f in
|
|
guard let self else {
|
|
return
|
|
}
|
|
f(.default)
|
|
|
|
self.deleteCollection(id: id)
|
|
})))
|
|
|
|
let contextController = ContextController(
|
|
presentationData: params.presentationData,
|
|
source: .extracted(GiftsExtractedContentSource(sourceNode: sourceNode)),
|
|
items: .single(ContextController.Items(content: .list(items))),
|
|
recognizer: nil,
|
|
gesture: gesture
|
|
)
|
|
self.parentController?.presentInGlobalOverlay(contextController)
|
|
}
|
|
|
|
|
|
|
|
func updateScrolling(interactive: Bool = false, transition: ComponentTransition) {
|
|
if let params = self.currentParams {
|
|
let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -10.0)
|
|
|
|
var topInset: CGFloat = 60.0
|
|
|
|
var canEditCollections = false
|
|
if self.peerId == self.context.account.peerId || self.canManage {
|
|
canEditCollections = true
|
|
}
|
|
|
|
let hasNonEmptyCollections = self.collections?.contains(where: { $0.count > 0 }) ?? false
|
|
if let collections = self.collections, !collections.isEmpty && (hasNonEmptyCollections || canEditCollections) {
|
|
var tabSelectorItems: [TabSelectorComponent.Item] = []
|
|
tabSelectorItems.append(TabSelectorComponent.Item(
|
|
id: AnyHashable(GiftCollection.all.rawValue),
|
|
title: params.presentationData.strings.PeerInfo_Gifts_Collections_All
|
|
))
|
|
|
|
var effectiveCollections: [StarGiftCollection] = collections
|
|
if let reorderedCollectionIds = self.reorderedCollectionIds {
|
|
var collectionMap: [Int32: StarGiftCollection] = [:]
|
|
for collection in collections {
|
|
collectionMap[collection.id] = collection
|
|
}
|
|
var reorderedCollections: [StarGiftCollection] = []
|
|
for id in reorderedCollectionIds {
|
|
if let collection = collectionMap[id] {
|
|
reorderedCollections.append(collection)
|
|
}
|
|
}
|
|
effectiveCollections = reorderedCollections
|
|
}
|
|
|
|
for collection in effectiveCollections {
|
|
if !canEditCollections && collection.count == 0 {
|
|
continue
|
|
}
|
|
tabSelectorItems.append(TabSelectorComponent.Item(
|
|
id: AnyHashable(GiftCollection.collection(collection.id).rawValue),
|
|
content: .component(AnyComponent(
|
|
CollectionTabItemComponent(
|
|
context: self.context,
|
|
icon: collection.icon.flatMap { .collection($0) },
|
|
title: collection.title,
|
|
theme: params.presentationData.theme
|
|
)
|
|
)),
|
|
isReorderable: collections.count > 1,
|
|
contextAction: { [weak self] sourceNode, gesture in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.openCollectionContextMenu(id: collection.id, sourceNode: sourceNode, gesture: gesture)
|
|
}
|
|
))
|
|
}
|
|
|
|
if canEditCollections {
|
|
tabSelectorItems.append(TabSelectorComponent.Item(
|
|
id: AnyHashable(GiftCollection.create.rawValue),
|
|
content: .component(AnyComponent(
|
|
CollectionTabItemComponent(
|
|
context: self.context,
|
|
icon: .add,
|
|
title: params.presentationData.strings.PeerInfo_Gifts_Collections_Add,
|
|
theme: params.presentationData.theme
|
|
)
|
|
)),
|
|
isReorderable: false
|
|
))
|
|
}
|
|
|
|
let tabSelectorSize = self.tabSelector.update(
|
|
transition: transition,
|
|
component: AnyComponent(TabSelectorComponent(
|
|
context: self.context,
|
|
colors: TabSelectorComponent.Colors(
|
|
foreground: params.presentationData.theme.list.itemSecondaryTextColor,
|
|
selection: params.presentationData.theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15),
|
|
simple: true
|
|
),
|
|
theme: params.presentationData.theme,
|
|
items: tabSelectorItems,
|
|
selectedId: AnyHashable(self.currentCollection.rawValue),
|
|
reorderItem: self.isReordering ? { [weak self] fromId, toId in
|
|
guard let self, var reorderedCollectionIds = self.reorderedCollectionIds else {
|
|
return
|
|
}
|
|
guard let sourceId = fromId.base as? Int32 else {
|
|
return
|
|
}
|
|
guard let targetId = toId.base as? Int32 else {
|
|
return
|
|
}
|
|
guard let sourceIndex = reorderedCollectionIds.firstIndex(of: sourceId), let targetIndex = reorderedCollectionIds.firstIndex(of: targetId) else {
|
|
return
|
|
}
|
|
reorderedCollectionIds[sourceIndex] = targetId
|
|
reorderedCollectionIds[targetIndex] = sourceId
|
|
self.reorderedCollectionIds = reorderedCollectionIds
|
|
|
|
self.updateScrolling(transition: .easeInOut(duration: 0.2))
|
|
} : nil,
|
|
setSelectedId: { [weak self] id in
|
|
guard let self, let idValue = id.base as? Int32 else {
|
|
return
|
|
}
|
|
|
|
let giftCollection = GiftCollection(rawValue: idValue)
|
|
if case .create = giftCollection {
|
|
self.createCollection()
|
|
} else {
|
|
self.setCurrentCollection(collection: giftCollection)
|
|
}
|
|
}
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: params.size.width - 10.0 * 2.0, height: 50.0)
|
|
)
|
|
if let tabSelectorView = self.tabSelector.view {
|
|
if tabSelectorView.superview == nil {
|
|
tabSelectorView.alpha = 1.0
|
|
self.scrollNode.view.addSubview(tabSelectorView)
|
|
|
|
if !transition.animation.isImmediate {
|
|
tabSelectorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
|
}
|
|
}
|
|
transition.setFrame(view: tabSelectorView, frame: CGRect(origin: CGPoint(x: floor((params.size.width - tabSelectorSize.width) / 2.0), y: 60.0), size: tabSelectorSize))
|
|
|
|
topInset += tabSelectorSize.height + 14.0
|
|
}
|
|
} else if let tabSelectorView = self.tabSelector.view {
|
|
tabSelectorView.alpha = 0.0
|
|
tabSelectorView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, completion: { _ in
|
|
tabSelectorView.removeFromSuperview()
|
|
})
|
|
}
|
|
|
|
var contentHeight = self.giftsListView.updateScrolling(topInset: topInset, visibleBounds: visibleBounds, transition: transition)
|
|
|
|
var bottomScrollInset: CGFloat = 0.0
|
|
let size = params.size
|
|
let sideInset = params.sideInset
|
|
let bottomInset = params.bottomInset
|
|
let presentationData = params.presentationData
|
|
|
|
let themeUpdated = self.theme !== presentationData.theme
|
|
self.theme = presentationData.theme
|
|
|
|
let panelBackground: NavigationBackgroundNode
|
|
let panelSeparator: ASDisplayNode
|
|
|
|
var panelVisibility = params.expandProgress < 1.0 ? 0.0 : 1.0
|
|
if !self.canGift || self.resultsAreEmpty {
|
|
panelVisibility = 0.0
|
|
}
|
|
|
|
let panelTransition: ComponentTransition = .immediate
|
|
if let current = self.panelBackground {
|
|
panelBackground = current
|
|
} else {
|
|
panelBackground = NavigationBackgroundNode(color: presentationData.theme.rootController.tabBar.backgroundColor)
|
|
self.addSubnode(panelBackground)
|
|
self.panelBackground = panelBackground
|
|
}
|
|
|
|
if let current = self.panelSeparator {
|
|
panelSeparator = current
|
|
} else {
|
|
panelSeparator = ASDisplayNode()
|
|
panelBackground.addSubnode(panelSeparator)
|
|
self.panelSeparator = panelSeparator
|
|
}
|
|
|
|
let panelButton: ComponentView<Empty>
|
|
if let current = self.panelButton {
|
|
panelButton = current
|
|
} else {
|
|
panelButton = ComponentView<Empty>()
|
|
self.panelButton = panelButton
|
|
}
|
|
|
|
let buttonSideInset = sideInset + 16.0
|
|
|
|
let buttonTitle: String
|
|
if self.peerId == self.context.account.peerId {
|
|
if case .all = self.currentCollection {
|
|
buttonTitle = params.presentationData.strings.PeerInfo_Gifts_Send
|
|
} else {
|
|
buttonTitle = params.presentationData.strings.PeerInfo_Gifts_AddGiftsButton
|
|
}
|
|
} else {
|
|
buttonTitle = params.presentationData.strings.PeerInfo_Gifts_SendGift
|
|
}
|
|
|
|
let buttonAttributedString = NSAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center)
|
|
let panelButtonSize = panelButton.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
ButtonComponent(
|
|
background: ButtonComponent.Background(
|
|
color: presentationData.theme.list.itemCheckColors.fillColor,
|
|
foreground: presentationData.theme.list.itemCheckColors.foregroundColor,
|
|
pressedColor: presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
|
|
),
|
|
content: AnyComponentWithIdentity(
|
|
id: AnyHashable(buttonAttributedString.string),
|
|
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
|
|
),
|
|
isEnabled: true,
|
|
action: { [weak self] in
|
|
self?.buttonPressed()
|
|
}
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: size.width - buttonSideInset * 2.0, height: 50.0)
|
|
)
|
|
|
|
var scrollOffset: CGFloat = max(0.0, size.height - params.visibleHeight)
|
|
|
|
let effectiveBottomInset = max(8.0, bottomInset)
|
|
var bottomPanelHeight = effectiveBottomInset + panelButtonSize.height + 8.0
|
|
if params.visibleHeight < 110.0 {
|
|
scrollOffset -= bottomPanelHeight
|
|
}
|
|
|
|
if let panelButtonView = panelButton.view {
|
|
if panelButtonView.superview == nil {
|
|
panelBackground.view.addSubview(panelButtonView)
|
|
}
|
|
panelButtonView.frame = CGRect(origin: CGPoint(x: buttonSideInset, y: 8.0), size: panelButtonSize)
|
|
}
|
|
|
|
if themeUpdated {
|
|
panelBackground.updateColor(color: presentationData.theme.rootController.tabBar.backgroundColor, transition: .immediate)
|
|
panelSeparator.backgroundColor = presentationData.theme.rootController.tabBar.separatorColor
|
|
}
|
|
|
|
if self.canManage {
|
|
bottomPanelHeight -= 9.0
|
|
|
|
let panelCheck: ComponentView<Empty>
|
|
if let current = self.panelCheck {
|
|
panelCheck = current
|
|
} else {
|
|
panelCheck = ComponentView<Empty>()
|
|
self.panelCheck = panelCheck
|
|
}
|
|
let checkTheme = CheckComponent.Theme(
|
|
backgroundColor: presentationData.theme.list.itemCheckColors.fillColor,
|
|
strokeColor: presentationData.theme.list.itemCheckColors.foregroundColor,
|
|
borderColor: presentationData.theme.list.itemCheckColors.strokeColor,
|
|
overlayBorder: false,
|
|
hasInset: false,
|
|
hasShadow: false
|
|
)
|
|
|
|
let panelCheckSize = panelCheck.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(
|
|
PlainButtonComponent(
|
|
content: AnyComponent(HStack([
|
|
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(CheckComponent(
|
|
theme: checkTheme,
|
|
size: CGSize(width: 22.0, height: 22.0),
|
|
selected: self.profileGifts.currentState?.notificationsEnabled ?? false
|
|
))),
|
|
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: presentationData.strings.PeerInfo_Gifts_ChannelNotify, font: Font.regular(17.0), textColor: presentationData.theme.list.itemPrimaryTextColor))
|
|
)))
|
|
],
|
|
spacing: 16.0
|
|
)),
|
|
effectAlignment: .center,
|
|
action: { [weak self] in
|
|
guard let self, let currentState = self.profileGifts.currentState else {
|
|
return
|
|
}
|
|
let enabled = !(currentState.notificationsEnabled ?? false)
|
|
self.profileGifts.toggleStarGiftsNotifications(enabled: enabled)
|
|
|
|
let animation = enabled ? "anim_profileunmute" : "anim_profilemute"
|
|
let text = enabled ? presentationData.strings.PeerInfo_Gifts_ChannelNotifyTooltip : presentationData.strings.PeerInfo_Gifts_ChannelNotifyDisabledTooltip
|
|
|
|
let controller = UndoOverlayController(
|
|
presentationData: presentationData,
|
|
content: .universal(animation: animation, scale: 0.075, colors: ["__allcolors__": UIColor.white], title: nil, text: text, customUndoText: nil, timeout: nil),
|
|
appearance: UndoOverlayController.Appearance(bottomInset: 53.0),
|
|
action: { _ in return true }
|
|
)
|
|
self.chatControllerInteraction.presentController(controller, nil)
|
|
|
|
self.updateScrolling(transition: .immediate)
|
|
},
|
|
animateAlpha: false,
|
|
animateScale: false
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: panelButtonSize
|
|
)
|
|
if let panelCheckView = panelCheck.view {
|
|
if panelCheckView.superview == nil {
|
|
panelBackground.view.addSubview(panelCheckView)
|
|
}
|
|
panelCheckView.frame = CGRect(origin: CGPoint(x: floor((size.width - panelCheckSize.width) / 2.0), y: 16.0), size: panelCheckSize)
|
|
}
|
|
if let panelButtonView = panelButton.view {
|
|
panelButtonView.isHidden = true
|
|
}
|
|
}
|
|
|
|
panelTransition.setFrame(view: panelBackground.view, frame: CGRect(x: 0.0, y: size.height - bottomPanelHeight - scrollOffset, width: size.width, height: bottomPanelHeight))
|
|
ComponentTransition.spring(duration: 0.4).setSublayerTransform(view: panelBackground.view, transform: CATransform3DMakeTranslation(0.0, bottomPanelHeight * (1.0 - panelVisibility), 0.0))
|
|
|
|
panelBackground.update(size: CGSize(width: size.width, height: bottomPanelHeight), transition: transition.containedViewLayoutTransition)
|
|
panelTransition.setFrame(view: panelSeparator.view, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: UIScreenPixel))
|
|
|
|
contentHeight += bottomPanelHeight
|
|
bottomScrollInset = bottomPanelHeight - 40.0
|
|
contentHeight += params.bottomInset
|
|
|
|
self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 50.0, left: 0.0, bottom: bottomScrollInset, right: 0.0)
|
|
|
|
let contentSize = CGSize(width: params.size.width, height: contentHeight)
|
|
if self.scrollNode.view.contentSize != contentSize {
|
|
self.scrollNode.view.contentSize = contentSize
|
|
}
|
|
}
|
|
|
|
let bottomContentOffset = max(0.0, self.scrollNode.view.contentSize.height - self.scrollNode.view.contentOffset.y - self.scrollNode.view.frame.height)
|
|
if interactive, bottomContentOffset < 200.0 {
|
|
self.giftsListView.loadMore()
|
|
}
|
|
}
|
|
|
|
@objc private func buttonPressed() {
|
|
if self.peerId == self.context.account.peerId || self.canManage {
|
|
if case let .collection(id) = self.currentCollection {
|
|
self.addGiftsToCollection(id: id)
|
|
} else {
|
|
let _ = (self.context.account.stateManager.contactBirthdays
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { [weak self] birthdays in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .settings(birthdays), completion: nil)
|
|
controller.navigationPresentation = .modal
|
|
self.chatControllerInteraction.navigationController()?.pushViewController(controller)
|
|
})
|
|
}
|
|
} else {
|
|
self.chatControllerInteraction.sendGift(self.peerId)
|
|
}
|
|
}
|
|
|
|
private func contextAction(gift: ProfileGiftsContext.State.StarGift, view: UIView, gesture: ContextGesture) {
|
|
guard let currentParams = self.currentParams else {
|
|
return
|
|
}
|
|
let presentationData = currentParams.presentationData
|
|
let strings = presentationData.strings
|
|
|
|
let canManage = self.peerId == self.context.account.peerId || self.canManage
|
|
var canReorder = false
|
|
if case .all = self.currentCollection, let currentState = self.profileGifts.currentState {
|
|
if case .All = currentState.filter {
|
|
for gift in currentState.gifts {
|
|
if gift.pinnedToTop {
|
|
canReorder = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
canReorder = true
|
|
}
|
|
|
|
let profileGifts: ProfileGiftsContext
|
|
switch self.currentCollection {
|
|
case let .collection(id):
|
|
profileGifts = self.profileGiftsCollections.giftsContextForCollection(id: id)
|
|
default:
|
|
profileGifts = self.profileGifts
|
|
}
|
|
|
|
var items: [ContextMenuItem] = []
|
|
if canManage {
|
|
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Context_AddToCollection, textLayout: .twoLinesMax, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Peer Info/Gifts/AddToCollection"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
|
|
var subItems: [ContextMenuItem] = []
|
|
|
|
subItems.append(.action(ContextMenuActionItem(text: strings.Common_Back, textColor: .primary, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
|
|
}, iconSource: nil, iconPosition: .left, action: { c, _ in
|
|
c?.popItems()
|
|
})))
|
|
|
|
subItems.append(.separator)
|
|
|
|
subItems.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Context_NewCollection, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Peer Info/Gifts/AddCollection"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { [weak self] c, f in
|
|
f(.default)
|
|
|
|
self?.createCollection(gifts: [gift])
|
|
})))
|
|
|
|
var entityFiles: [Int64: TelegramMediaFile] = [:]
|
|
|
|
if let collections = self?.collections {
|
|
for collection in collections {
|
|
if let file = collection.icon {
|
|
entityFiles[file.fileId.id] = file
|
|
}
|
|
}
|
|
|
|
for collection in collections {
|
|
let title: String
|
|
var entities: [MessageTextEntity] = []
|
|
if let icon = collection.icon {
|
|
title = "# \(collection.title)"
|
|
entities = [
|
|
MessageTextEntity(
|
|
range: 0..<1,
|
|
type: .CustomEmoji(stickerPack: nil, fileId: icon.fileId.id)
|
|
)
|
|
]
|
|
} else {
|
|
title = collection.title
|
|
}
|
|
|
|
let isAdded = gift.collectionIds?.contains(collection.id) ?? false
|
|
|
|
subItems.append(.action(ContextMenuActionItem(text: title, entities: entities, entityFiles: entityFiles, enableEntityAnimations: false, icon: { theme in
|
|
return entities.isEmpty ? generateTintedImage(image: UIImage(bundleImageName: "Peer Info/Gifts/Collection"), color: theme.contextMenu.primaryColor) : (isAdded ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil)
|
|
}, iconPosition: collection.icon == nil ? .left : .right, action: { [weak self] _, f in
|
|
f(.default)
|
|
|
|
if isAdded, let giftReference = gift.reference {
|
|
let _ = self?.profileGiftsCollections.removeGifts(id: collection.id, gifts: [giftReference]).start()
|
|
} else {
|
|
let _ = self?.profileGiftsCollections.addGifts(id: collection.id, gifts: [gift]).start()
|
|
}
|
|
})))
|
|
}
|
|
}
|
|
|
|
c?.pushItems(items: .single(ContextController.Items(content: .list(subItems))))
|
|
})))
|
|
items.append(.separator)
|
|
}
|
|
|
|
if canManage {
|
|
if case .unique = gift.gift, case .all = self.currentCollection {
|
|
items.append(.action(ContextMenuActionItem(text: gift.pinnedToTop ? strings.PeerInfo_Gifts_Context_Unpin : strings.PeerInfo_Gifts_Context_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: gift.pinnedToTop ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
|
|
c?.dismiss(completion: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let pinnedToTop = !gift.pinnedToTop
|
|
guard let reference = gift.reference else {
|
|
return
|
|
}
|
|
|
|
if pinnedToTop && self.giftsListView.pinnedReferences.count >= self.giftsListView.maxPinnedCount {
|
|
self.displayUnpinScreen(gift: gift)
|
|
return
|
|
}
|
|
|
|
profileGifts.updateStarGiftPinnedToTop(reference: reference, pinnedToTop: pinnedToTop)
|
|
|
|
let toastTitle: String?
|
|
let toastText: String
|
|
if !pinnedToTop {
|
|
toastTitle = nil
|
|
toastText = strings.PeerInfo_Gifts_ToastUnpinned_Text
|
|
} else {
|
|
var title = ""
|
|
if case let .unique(uniqueGift) = gift.gift {
|
|
title = "\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, presentationData.dateTimeFormat.groupingSeparator))"
|
|
}
|
|
toastTitle = strings.PeerInfo_Gifts_ToastPinned_TitleNew(title).string
|
|
toastText = strings.PeerInfo_Gifts_ToastPinned_Text
|
|
}
|
|
self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: !pinnedToTop ? "anim_toastunpin" : "anim_toastpin", scale: 0.06, colors: [:], title: toastTitle, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
|
})
|
|
})))
|
|
}
|
|
|
|
var isReorderableGift = false
|
|
if case .unique = gift.gift {
|
|
isReorderableGift = true
|
|
} else if case .collection = self.currentCollection {
|
|
isReorderableGift = true
|
|
}
|
|
|
|
if isReorderableGift && canManage && canReorder {
|
|
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Context_Reorder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
|
|
c?.dismiss(completion: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.beginReordering()
|
|
})
|
|
})))
|
|
}
|
|
|
|
if case let .unique(uniqueGift) = gift.gift, self.peerId == self.context.account.peerId {
|
|
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Context_Wear, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Peer Info/WearIcon"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
|
|
c?.dismiss(completion: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if self.context.isPremium {
|
|
let _ = self.context.engine.accountData.setStarGiftStatus(starGift: uniqueGift, expirationDate: nil).startStandalone()
|
|
} else {
|
|
let text = strings.Gift_View_TooltipPremiumWearing
|
|
let tooltipController = UndoOverlayController(
|
|
presentationData: presentationData,
|
|
content: .premiumPaywall(title: nil, text: text, customUndoText: nil, timeout: nil, linkAction: nil),
|
|
position: .bottom,
|
|
animateInAsReplacement: false,
|
|
appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0),
|
|
action: { [weak self] action in
|
|
if let self, case .info = action {
|
|
let premiumController = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .messageEffects, forceDark: false, dismissed: nil)
|
|
self.parentController?.push(premiumController)
|
|
}
|
|
return false
|
|
}
|
|
)
|
|
self.parentController?.present(tooltipController, in: .current)
|
|
}
|
|
})
|
|
})))
|
|
}
|
|
}
|
|
|
|
if case let .unique(gift) = gift.gift {
|
|
let link = "https://t.me/nft/\(gift.slug)"
|
|
|
|
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Context_CopyLink, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
|
|
c?.dismiss(completion: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
UIPasteboard.general.string = link
|
|
|
|
self.parentController?.present(UndoOverlayController(presentationData: currentParams.presentationData, content: .linkCopied(title: nil, text: currentParams.presentationData.strings.Conversation_LinkCopied), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .current)
|
|
})
|
|
})))
|
|
|
|
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Context_Share, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
|
|
c?.dismiss(completion: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let context = self.context
|
|
let shareController = context.sharedContext.makeShareController(
|
|
context: context,
|
|
subject: .url(link),
|
|
forceExternal: false,
|
|
shareStory: { [weak self] in
|
|
guard let self, let parentController = self.parentController else {
|
|
return
|
|
}
|
|
Queue.mainQueue().after(0.15) {
|
|
let controller = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .gift(gift), parentController: parentController)
|
|
parentController.push(controller)
|
|
}
|
|
},
|
|
enqueued: { [weak self] peerIds, _ in
|
|
let _ = (context.engine.data.get(
|
|
EngineDataList(
|
|
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)
|
|
)
|
|
)
|
|
|> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in
|
|
guard let self, let parentController = self.parentController else {
|
|
return
|
|
}
|
|
|
|
let peers = peerList.compactMap { $0 }
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
let text: String
|
|
var savedMessages = false
|
|
if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId {
|
|
text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One
|
|
savedMessages = true
|
|
} else {
|
|
if peers.count == 1, let peer = peers.first {
|
|
var peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
|
peerName = peerName.replacingOccurrences(of: "**", with: "")
|
|
text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string
|
|
} else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last {
|
|
var firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
|
firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "")
|
|
var secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
|
secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "")
|
|
text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string
|
|
} else if let peer = peers.first {
|
|
var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
|
peerName = peerName.replacingOccurrences(of: "**", with: "")
|
|
text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string
|
|
} else {
|
|
text = ""
|
|
}
|
|
}
|
|
|
|
parentController.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: true, animateInAsReplacement: false, action: { [weak self] action in
|
|
if savedMessages, action == .info {
|
|
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
|
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
|
guard let peer, let navigationController = self?.parentController?.navigationController as? NavigationController else {
|
|
return
|
|
}
|
|
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), subject: nil, botStart: nil, updateTextInputState: nil, keepStack: .always, useExisting: true, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: nil, animated: true))
|
|
})
|
|
}
|
|
return false
|
|
}, additionalView: nil), in: .current)
|
|
})
|
|
},
|
|
actionCompleted: { [weak self] in
|
|
self?.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .current)
|
|
}
|
|
)
|
|
self.parentController?.present(shareController, in: .window(.root))
|
|
})
|
|
})))
|
|
}
|
|
|
|
if canManage {
|
|
items.append(.action(ContextMenuActionItem(text: gift.savedToProfile ? strings.PeerInfo_Gifts_Context_Hide : strings.PeerInfo_Gifts_Context_Show, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: gift.savedToProfile ? "Peer Info/HideIcon" : "Peer Info/ShowIcon"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
|
|
c?.dismiss(completion: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if let reference = gift.reference {
|
|
let added = !gift.savedToProfile
|
|
profileGifts.updateStarGiftAddedToProfile(reference: reference, added: added)
|
|
|
|
var animationFile: TelegramMediaFile?
|
|
switch gift.gift {
|
|
case let .generic(gift):
|
|
animationFile = gift.file
|
|
case let .unique(gift):
|
|
for attribute in gift.attributes {
|
|
if case let .model(_, file, _) = attribute {
|
|
animationFile = file
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
let text: String
|
|
if self.peerId.namespace == Namespaces.Peer.CloudChannel {
|
|
text = added ? presentationData.strings.Gift_Displayed_ChannelText : presentationData.strings.Gift_Hidden_ChannelText
|
|
} else {
|
|
text = added ? presentationData.strings.Gift_Displayed_NewText : presentationData.strings.Gift_Hidden_NewText
|
|
}
|
|
|
|
if let animationFile {
|
|
let resultController = UndoOverlayController(
|
|
presentationData: presentationData,
|
|
content: .sticker(context: context, file: animationFile, loop: false, title: nil, text: text, undoText: nil, customAction: nil),
|
|
elevatedLayout: true,
|
|
action: { _ in
|
|
return true
|
|
}
|
|
)
|
|
self.parentController?.present(resultController, in: .window(.root))
|
|
}
|
|
}
|
|
})
|
|
})))
|
|
|
|
if case let .unique(uniqueGift) = gift.gift {
|
|
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Context_Transfer, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Peer Info/TransferIcon"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
|
|
c?.dismiss(completion: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let context = self.context
|
|
let _ = (context.account.stateManager.contactBirthdays
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { [weak self] birthdays in
|
|
guard let self, let reference = gift.reference else {
|
|
return
|
|
}
|
|
var showSelf = false
|
|
if self.peerId.namespace == Namespaces.Peer.CloudChannel {
|
|
showSelf = true
|
|
}
|
|
let transferStars = gift.transferStars ?? 0
|
|
let controller = context.sharedContext.makePremiumGiftController(context: context, source: .starGiftTransfer(birthdays, reference, uniqueGift, transferStars, gift.canExportDate, showSelf), completion: { peerIds in
|
|
guard let peerId = peerIds.first else {
|
|
return .complete()
|
|
}
|
|
Queue.mainQueue().after(1.5, {
|
|
if transferStars > 0 {
|
|
context.starsContext?.load(force: true)
|
|
}
|
|
})
|
|
return profileGifts.transferStarGift(prepaid: transferStars == 0, reference: reference, peerId: peerId)
|
|
})
|
|
self.parentController?.push(controller)
|
|
})
|
|
})
|
|
})))
|
|
}
|
|
}
|
|
|
|
if case let .collection(id) = self.currentCollection {
|
|
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Context_RemoveFromCollection, textColor: .destructive, textLayout: .twoLinesMax, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Peer Info/Gifts/RemoveFromCollection"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, f in
|
|
f(.default)
|
|
|
|
if let reference = gift.reference {
|
|
let _ = self?.profileGiftsCollections.removeGifts(id: id, gifts: [reference]).start()
|
|
}
|
|
})))
|
|
}
|
|
|
|
guard !items.isEmpty else {
|
|
return
|
|
}
|
|
|
|
let previewController = GiftContextPreviewController(context: self.context, gift: gift)
|
|
let contextController = ContextController(
|
|
context: self.context,
|
|
presentationData: currentParams.presentationData,
|
|
source: .controller(ContextControllerContentSourceImpl(controller: previewController, sourceView: view)),
|
|
items: .single(ContextController.Items(content: .list(items))), gesture: gesture
|
|
)
|
|
self.parentController?.presentInGlobalOverlay(contextController)
|
|
}
|
|
|
|
public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
|
self.currentParams = (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData)
|
|
self.presentationDataPromise.set(.single(presentationData))
|
|
|
|
self.backgroundNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
|
|
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 48.0), size: size))
|
|
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
|
|
|
|
let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -10.0)
|
|
|
|
let contentHeight = self.giftsListView.update(size: size, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: synchronous, visibleBounds: visibleBounds, transition: transition)
|
|
transition.updateFrame(view: self.giftsListView, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: max(size.height, contentHeight))))
|
|
|
|
if isScrollingLockedAtTop {
|
|
self.scrollNode.view.contentOffset = .zero
|
|
}
|
|
self.scrollNode.view.isScrollEnabled = !isScrollingLockedAtTop
|
|
|
|
self.updateScrolling(transition: ComponentTransition(transition))
|
|
}
|
|
|
|
public func findLoadedMessage(id: MessageId) -> Message? {
|
|
return nil
|
|
}
|
|
|
|
public func updateHiddenMedia() {
|
|
}
|
|
|
|
public func transferVelocity(_ velocity: CGFloat) {
|
|
if velocity > 0.0 {
|
|
// self.scrollNode.transferVelocity(velocity)
|
|
}
|
|
}
|
|
|
|
public func cancelPreviewGestures() {
|
|
}
|
|
|
|
public func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
|
return nil
|
|
}
|
|
|
|
public func addToTransitionSurface(view: UIView) {
|
|
}
|
|
|
|
public func updateSelectedMessages(animated: Bool) {
|
|
}
|
|
}
|
|
|
|
private func cancelContextGestures(view: UIView) {
|
|
if let gestureRecognizers = view.gestureRecognizers {
|
|
for gesture in gestureRecognizers {
|
|
if let gesture = gesture as? ContextGesture {
|
|
gesture.cancel()
|
|
}
|
|
}
|
|
}
|
|
for subview in view.subviews {
|
|
cancelContextGestures(view: subview)
|
|
}
|
|
}
|
|
|
|
private final class CollectionTabItemComponent: Component {
|
|
typealias EnvironmentType = TabSelectorComponent.ItemEnvironment
|
|
|
|
enum Icon: Equatable {
|
|
case collection(TelegramMediaFile)
|
|
case add
|
|
}
|
|
|
|
let context: AccountContext
|
|
let icon: Icon?
|
|
let title: String
|
|
let theme: PresentationTheme
|
|
|
|
init(
|
|
context: AccountContext,
|
|
icon: Icon?,
|
|
title: String,
|
|
theme: PresentationTheme
|
|
) {
|
|
self.context = context
|
|
self.icon = icon
|
|
self.title = title
|
|
self.theme = theme
|
|
}
|
|
|
|
static func ==(lhs: CollectionTabItemComponent, rhs: CollectionTabItemComponent) -> Bool {
|
|
if lhs.icon != rhs.icon {
|
|
return false
|
|
}
|
|
if lhs.title != rhs.title {
|
|
return false
|
|
}
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
private let title = ComponentView<Empty>()
|
|
private let icon = ComponentView<Empty>()
|
|
private var iconLayer: InlineStickerItemLayer?
|
|
|
|
private var component: CollectionTabItemComponent?
|
|
|
|
func update(component: CollectionTabItemComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
|
self.component = component
|
|
|
|
let iconSpacing: CGFloat = 3.0
|
|
|
|
let titleSize = self.title.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(14.0), textColor: .white))
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
|
)
|
|
|
|
let tintColor = component.theme.list.itemSecondaryTextColor
|
|
|
|
var iconOffset: CGFloat = 0.0
|
|
var iconSize = CGSize()
|
|
if let icon = component.icon {
|
|
switch icon {
|
|
case let .collection(file):
|
|
iconSize = CGSize(width: 16.0, height: 16.0)
|
|
|
|
let iconLayer: InlineStickerItemLayer
|
|
if let current = self.iconLayer {
|
|
iconLayer = current
|
|
} else {
|
|
iconLayer = InlineStickerItemLayer(
|
|
context: component.context,
|
|
userLocation: .other,
|
|
attemptSynchronousLoad: true,
|
|
emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file),
|
|
file: file,
|
|
cache: component.context.animationCache,
|
|
renderer: component.context.animationRenderer,
|
|
placeholderColor: component.theme.list.mediaPlaceholderColor,
|
|
pointSize: iconSize,
|
|
loopCount: 1
|
|
)
|
|
self.layer.addSublayer(iconLayer)
|
|
self.iconLayer = iconLayer
|
|
}
|
|
let iconFrame = CGRect(origin: CGPoint(x: iconOffset, y: floorToScreenPixels((titleSize.height - iconSize.height) * 0.5)), size: iconSize)
|
|
iconLayer.frame = iconFrame
|
|
case .add:
|
|
iconSize = self.icon.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(BundleIconComponent(
|
|
name: "Chat/Input/Media/PanelBadgeAdd",
|
|
tintColor: tintColor
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 100.0, height: 100.0)
|
|
)
|
|
let iconFrame = CGRect(origin: CGPoint(x: iconOffset, y: floorToScreenPixels((titleSize.height - iconSize.height) * 0.5)), size: iconSize)
|
|
if let iconView = self.icon.view {
|
|
if iconView.superview == nil {
|
|
iconView.isUserInteractionEnabled = false
|
|
self.addSubview(iconView)
|
|
}
|
|
iconView.frame = iconFrame
|
|
}
|
|
}
|
|
|
|
iconOffset += iconSize.width + iconSpacing
|
|
} else {
|
|
if let iconLayer = self.iconLayer {
|
|
self.iconLayer = nil
|
|
iconLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
|
iconLayer.removeFromSuperlayer()
|
|
})
|
|
iconLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
|
}
|
|
if let iconView = self.icon.view {
|
|
iconView.removeFromSuperview()
|
|
}
|
|
}
|
|
|
|
let titleFrame = CGRect(origin: CGPoint(x: iconOffset, y: 0.0), size: titleSize)
|
|
if let titleView = self.title.view {
|
|
if titleView.superview == nil {
|
|
titleView.isUserInteractionEnabled = false
|
|
self.addSubview(titleView)
|
|
}
|
|
titleView.frame = titleFrame
|
|
|
|
transition.setTintColor(layer: titleView.layer, color: tintColor)
|
|
}
|
|
|
|
let size: CGSize
|
|
if let _ = component.icon {
|
|
size = CGSize(width: iconSize.width + iconSpacing + titleSize.width, height: titleSize.height)
|
|
} else {
|
|
size = titleSize
|
|
}
|
|
|
|
return size
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
|
|
let controller: ViewController
|
|
weak var sourceView: UIView?
|
|
|
|
let navigationController: NavigationController? = nil
|
|
|
|
let passthroughTouches: Bool = false
|
|
|
|
init(controller: ViewController, sourceView: UIView?) {
|
|
self.controller = controller
|
|
self.sourceView = sourceView
|
|
}
|
|
|
|
func transitionInfo() -> ContextControllerTakeControllerInfo? {
|
|
let sourceView = self.sourceView
|
|
|
|
return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceView] in
|
|
if let sourceView {
|
|
return (sourceView, sourceView.bounds)
|
|
} else {
|
|
return nil
|
|
}
|
|
})
|
|
}
|
|
|
|
func animatedIn() {
|
|
self.controller.didAppearInContextPreview()
|
|
}
|
|
}
|
|
|
|
private final class GiftsExtractedContentSource: ContextExtractedContentSource {
|
|
let keepInPlace: Bool = false
|
|
let ignoreContentTouches: Bool = false
|
|
let blurBackground: Bool = true
|
|
|
|
private let sourceNode: ContextExtractedContentContainingNode
|
|
|
|
init(sourceNode: ContextExtractedContentContainingNode) {
|
|
self.sourceNode = sourceNode
|
|
}
|
|
|
|
func takeView() -> ContextControllerTakeViewInfo? {
|
|
return ContextControllerTakeViewInfo(containingItem: .node(self.sourceNode), contentAreaInScreenSpace: UIScreen.main.bounds)
|
|
}
|
|
|
|
func putBack() -> ContextControllerPutBackViewInfo? {
|
|
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
|
|
}
|
|
}
|