Merge branch 'gift-resale' of gitlab.com:peter-iakovlev/telegram-ios into gift-resale

# Conflicts:
#	submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift
This commit is contained in:
Mikhail Filimonov 2025-04-18 12:59:52 +01:00
commit 037890cfa5
25 changed files with 1736 additions and 673 deletions

View File

@ -1178,7 +1178,7 @@ public protocol SharedAccountContext: AnyObject {
func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsWithdrawalScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarGiftResellScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarGiftResellScreen(context: AccountContext, update: Bool, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController
func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> ViewController
func makeStarsIntroScreen(context: AccountContext) -> ViewController

View File

@ -41,6 +41,7 @@ public struct AttachmentMainButtonState {
public let progress: Progress
public let isEnabled: Bool
public let hasShimmer: Bool
public let iconName: String?
public let position: Position?
public init(
@ -53,6 +54,7 @@ public struct AttachmentMainButtonState {
progress: Progress,
isEnabled: Bool,
hasShimmer: Bool,
iconName: String? = nil,
position: Position? = nil
) {
self.text = text
@ -64,6 +66,7 @@ public struct AttachmentMainButtonState {
self.progress = progress
self.isEnabled = isEnabled
self.hasShimmer = hasShimmer
self.iconName = iconName
self.position = position
}

View File

@ -476,6 +476,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
private var size: CGSize?
private let backgroundAnimationNode: ASImageNode
private var iconNode: ASImageNode?
fileprivate let textNode: ImmediateTextNode
private var badgeNode: BadgeNode?
private let statusNode: SemanticStatusNode
@ -781,6 +782,25 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
badgeNode.removeFromSupernode()
}
if let iconName = state.iconName {
let iconNode: ASImageNode
if let current = self.iconNode {
iconNode = current
} else {
iconNode = ASImageNode()
iconNode.displaysAsynchronously = false
iconNode.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: state.textColor)
self.addSubnode(iconNode)
}
if let iconSize = iconNode.image?.size {
textFrame.origin.x += (iconSize.width + 6.0) / 2.0
iconNode.frame = CGRect(origin: CGPoint(x: textFrame.minX - iconSize.width - 6.0, y: textFrame.minY + floorToScreenPixels((textFrame.height - iconSize.height) * 0.5)), size: iconSize)
}
} else if let iconNode = self.iconNode {
self.iconNode = nil
iconNode.removeFromSupernode()
}
if self.textNode.frame.width.isZero {
self.textNode.frame = textFrame
} else {
@ -795,7 +815,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
self.transitionFromProgress()
}
}
if let shimmerView = self.shimmerView, let borderView = self.borderView, let borderMaskView = self.borderMaskView, let borderShimmerView = self.borderShimmerView {
let buttonFrame = CGRect(origin: .zero, size: size)
let buttonWidth = size.width

View File

@ -2145,6 +2145,8 @@ public protocol ContextReferenceContentSource: AnyObject {
var shouldBeDismissed: Signal<Bool, NoError> { get }
var forceDisplayBelowKeyboard: Bool { get }
func transitionInfo() -> ContextControllerReferenceViewInfo?
}
@ -2153,6 +2155,10 @@ public extension ContextReferenceContentSource {
return false
}
var forceDisplayBelowKeyboard: Bool {
return false
}
var shouldBeDismissed: Signal<Bool, NoError> {
return .single(false)
}
@ -2744,7 +2750,9 @@ public final class ContextController: ViewController, StandalonePresentableContr
}
public func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?) {
if viewTreeContainsFirstResponder(view: self.view) {
if let mainSource = self.configuration.sources.first(where: { $0.id == self.configuration.initialId }), case let .reference(source) = mainSource.source, source.forceDisplayBelowKeyboard {
} else if viewTreeContainsFirstResponder(view: self.view) {
self.dismissOnInputClose = (result, completion)
self.view.endEditing(true)
return

View File

@ -273,7 +273,7 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
return super.hitTest(point, with: event)
}
func setItem(item: ContextMenuActionItem) {
public func setItem(item: ContextMenuActionItem) {
self.item = item
self.accessibilityLabel = item.text
}
@ -363,6 +363,8 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
return ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: fileId, enableAnimation: true), range: entity.range)
} else if case .Bold = entity.type {
return ChatTextInputStateTextAttribute(type: .bold, range: entity.range)
} else if case .Italic = entity.type {
return ChatTextInputStateTextAttribute(type: .italic, range: entity.range)
}
return nil
})
@ -373,6 +375,8 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
], range: NSRange(location: 0, length: result.length))
for attribute in inputStateText.attributes {
if case .bold = attribute.type {
result.addAttribute(NSAttributedString.Key.font, value: Font.semibold(presentationData.listsFontSize.baseDisplaySize), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
} else if case .italic = attribute.type {
result.addAttribute(NSAttributedString.Key.font, value: Font.semibold(15.0), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
}
}

View File

@ -500,6 +500,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
func wantsDisplayBelowKeyboard() -> Bool {
if let reactionContextNode = self.reactionContextNode {
return reactionContextNode.wantsDisplayBelowKeyboard()
} else if case let .reference(source) = self.source {
return source.forceDisplayBelowKeyboard
} else {
return false
}

View File

@ -1823,6 +1823,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
private var isDismissing = false
fileprivate let mainButtonStatePromise = Promise<AttachmentMainButtonState?>(nil)
fileprivate let secondaryButtonStatePromise = Promise<AttachmentMainButtonState?>(nil)
private let mainButtonAction: (() -> Void)?
@ -2380,9 +2381,23 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
transition.updateAlpha(node: self.moreButtonNode.iconNode, alpha: moreIsVisible ? 1.0 : 0.0)
transition.updateTransformScale(node: self.moreButtonNode.iconNode, scale: moreIsVisible ? 1.0 : 0.1)
//if self. {
//self.mainButtonStatePromise.set(.single(AttachmentMainButtonState(text: "Add", badge: "\(count)", font: .bold, background: .color(self.presentationData.theme.actionSheet.controlAccentColor), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, isVisible: count > 0, progress: .none, isEnabled: true, hasShimmer: false)))
//}
if self.selectionCount > 0 {
//TODO:localize
var text = "Create 1 Story"
if self.selectionCount > 1 {
text = "Create \(self.selectionCount) Stories"
}
self.mainButtonStatePromise.set(.single(AttachmentMainButtonState(text: text, badge: nil, font: .bold, background: .color(self.presentationData.theme.actionSheet.controlAccentColor), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, position: .top)))
if self.selectionCount > 1 && self.selectionCount <= 6 {
self.secondaryButtonStatePromise.set(.single(AttachmentMainButtonState(text: "Combine into Collage", badge: nil, font: .regular, background: .color(.clear), textColor: self.presentationData.theme.actionSheet.controlAccentColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, iconName: "Media Editor/Collage", position: .bottom)))
} else {
self.secondaryButtonStatePromise.set(.single(nil))
}
} else {
self.mainButtonStatePromise.set(.single(nil))
self.secondaryButtonStatePromise.set(.single(nil))
}
}
private func updateThemeAndStrings() {
@ -2933,6 +2948,10 @@ final class MediaPickerContext: AttachmentMediaPickerContext {
return self.controller?.mainButtonStatePromise.get() ?? .single(nil)
}
public var secondaryButtonState: Signal<AttachmentMainButtonState?, NoError> {
return self.controller?.secondaryButtonStatePromise.get() ?? .single(nil)
}
init(controller: MediaPickerScreenImpl) {
self.controller = controller
}
@ -2952,6 +2971,10 @@ final class MediaPickerContext: AttachmentMediaPickerContext {
func mainButtonAction() {
self.controller?.mainButtonPressed()
}
func secondaryButtonAction() {
self.controller?.mainButtonPressed()
}
}
private final class MediaPickerContextReferenceContentSource: ContextReferenceContentSource {

View File

@ -1459,23 +1459,26 @@ private final class ProfileGiftsContextImpl {
return _internal_transferStarGift(account: self.account, prepaid: prepaid, reference: reference, peerId: peerId)
}
func buyStarGift(gift inputGift: TelegramCore.StarGift, peerId: EnginePeer.Id) -> Signal<Never, BuyStarGiftError> {
var gift = self.gifts.first(where: { $0.gift == inputGift })
if gift == nil {
gift = self.filteredGifts.first(where: { $0.gift == inputGift })
}
guard case let .unique(uniqueGift) = gift?.gift else {
return .complete()
}
func buyStarGift(slug: String, peerId: EnginePeer.Id) -> Signal<Never, BuyStarGiftError> {
if let count = self.count {
self.count = max(0, count - 1)
}
self.gifts.removeAll(where: { $0.gift == inputGift })
self.filteredGifts.removeAll(where: { $0.gift == inputGift })
self.gifts.removeAll(where: { gift in
if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug {
return true
}
return false
})
self.filteredGifts.removeAll(where: { gift in
if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug {
return true
}
return false
})
self.pushState()
return _internal_buyStarGift(account: self.account, slug: uniqueGift.slug, peerId: peerId)
return _internal_buyStarGift(account: self.account, slug: slug, peerId: peerId)
}
func removeStarGift(gift: TelegramCore.StarGift) {
@ -1899,11 +1902,11 @@ public final class ProfileGiftsContext {
}
}
public func buyStarGift(gift: TelegramCore.StarGift, peerId: EnginePeer.Id) -> Signal<Never, BuyStarGiftError> {
public func buyStarGift(slug: String, peerId: EnginePeer.Id) -> Signal<Never, BuyStarGiftError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.buyStarGift(gift: gift, peerId: peerId).start(error: { error in
disposable.set(impl.buyStarGift(slug: slug, peerId: peerId).start(error: { error in
subscriber.putError(error)
}, completed: {
subscriber.putCompletion()
@ -2308,7 +2311,7 @@ private final class ResaleGiftsContextImpl {
private let disposable = MetaDisposable()
private var sorting: ResaleGiftsContext.Sorting = .date
private var sorting: ResaleGiftsContext.Sorting = .value
private var filterAttributes: [ResaleGiftsContext.Attribute] = []
private var gifts: [StarGift] = []
@ -2431,7 +2434,15 @@ private final class ResaleGiftsContextImpl {
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
return (gifts.compactMap { StarGift(apiStarGift: $0) }, resultAttributes, attributeCount, count, nextOffset)
var mappedGifts: [StarGift] = []
for gift in gifts {
if let mappedGift = StarGift(apiStarGift: gift), case let .unique(uniqueGift) = mappedGift, let resellStars = uniqueGift.resellStars, resellStars > 0 {
mappedGifts.append(mappedGift)
}
}
return (mappedGifts, resultAttributes, attributeCount, count, nextOffset)
}
}
}
@ -2444,9 +2455,7 @@ private final class ResaleGiftsContextImpl {
if initialNextOffset == nil || reload {
self.gifts = gifts
} else {
for gift in gifts {
self.gifts.append(gift)
}
self.gifts.append(contentsOf: gifts)
}
let updatedCount = max(Int32(self.gifts.count), count)
@ -2473,6 +2482,11 @@ private final class ResaleGiftsContextImpl {
self.loadMore()
}
func removeStarGift(gift: TelegramCore.StarGift) {
self.gifts.removeAll(where: { $0 == gift })
self.pushState()
}
func updateSorting(_ sorting: ResaleGiftsContext.Sorting) {
guard self.sorting != sorting else {
return
@ -2571,6 +2585,12 @@ public final class ResaleGiftsContext {
impl.updateFilterAttributes(attributes)
}
}
public func removeStarGift(gift: TelegramCore.StarGift) {
self.impl.with { impl in
impl.removeStarGift(gift: gift)
}
}
public var currentState: ResaleGiftsContext.State? {
var state: ResaleGiftsContext.State?

View File

@ -125,7 +125,7 @@ public extension TelegramEngine {
return _internal_transferStarGift(account: self.account, prepaid: prepaid, reference: reference, peerId: peerId)
}
public func buyStarGift(prepaid: Bool, slug: String, peerId: EnginePeer.Id) -> Signal<Never, BuyStarGiftError> {
public func buyStarGift(slug: String, peerId: EnginePeer.Id) -> Signal<Never, BuyStarGiftError> {
return _internal_buyStarGift(account: self.account, slug: slug, peerId: peerId)
}

View File

@ -2726,9 +2726,13 @@ public class CameraScreenImpl: ViewController, CameraScreen {
self.additionalPreviewView.isEnabled = false
self.collageView?.isEnabled = false
#if targetEnvironment(simulator)
#else
Queue.mainQueue().after(0.3) {
self.previewBlurPromise.set(true)
}
#endif
self.camera?.stopCapture()
self.cameraIsActive = false
@ -3627,12 +3631,17 @@ public class CameraScreenImpl: ViewController, CameraScreen {
if self.cameraState.isCollageEnabled, let collage = self.node.collage {
selectionLimit = collage.grid.count - collage.results.count
} else {
selectionLimit = 6
if self.cameraState.isCollageEnabled {
selectionLimit = 6
} else {
selectionLimit = 10
}
}
//TODO:unmock
controller = self.context.sharedContext.makeStoryMediaPickerScreen(
context: self.context,
isDark: true,
forCollage: self.cameraState.isCollageEnabled,
forCollage: self.cameraState.isCollageEnabled || "".isEmpty,
selectionLimit: selectionLimit,
getSourceRect: { [weak self] in
if let self {

View File

@ -44,6 +44,7 @@ swift_library(
"//submodules/TelegramUI/Components/Gifts/GiftSetupScreen",
"//submodules/TelegramUI/Components/Gifts/GiftViewScreen",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/TextFieldComponent",
],
visibility = [
"//visibility:public",

View File

@ -24,21 +24,24 @@ public final class FilterSelectorComponent: Component {
public struct Item: Equatable {
public var id: AnyHashable
public var iconName: String?
public var title: String
public var action: (UIView) -> Void
public init(
id: AnyHashable,
iconName: String? = nil,
title: String,
action: @escaping (UIView) -> Void
) {
self.id = id
self.iconName = iconName
self.title = title
self.action = action
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
return lhs.id == rhs.id && lhs.title == rhs.title
return lhs.id == rhs.id && lhs.iconName == rhs.iconName && lhs.title == rhs.title
}
}
@ -142,6 +145,7 @@ public final class FilterSelectorComponent: Component {
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(ItemComponent(
context: component.context,
iconName: item.iconName,
text: item.title,
font: itemFont,
color: component.colors.foreground,
@ -231,6 +235,7 @@ extension CGRect {
private final class ItemComponent: CombinedComponent {
let context: AccountContext?
let iconName: String?
let text: String
let font: UIFont
let color: UIColor
@ -238,12 +243,14 @@ private final class ItemComponent: CombinedComponent {
init(
context: AccountContext?,
iconName: String?,
text: String,
font: UIFont,
color: UIColor,
backgroundColor: UIColor
) {
self.context = context
self.iconName = iconName
self.text = text
self.font = font
self.color = color
@ -254,6 +261,9 @@ private final class ItemComponent: CombinedComponent {
if lhs.context !== rhs.context {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.text != rhs.text {
return false
}
@ -297,17 +307,22 @@ private final class ItemComponent: CombinedComponent {
let icon = icon.update(
component: BundleIconComponent(
name: "Item List/ExpandableSelectorArrows",
tintColor: component.color
name: component.iconName ?? "Item List/ExpandableSelectorArrows",
tintColor: component.color,
maxSize: component.iconName != nil ? CGSize(width: 22.0, height: 22.0) : nil
),
availableSize: CGSize(width: 100, height: 100),
transition: .immediate
)
let padding: CGFloat = 12.0
var leftPadding = padding
if let _ = component.iconName {
leftPadding -= 4.0
}
let spacing: CGFloat = 4.0
let totalWidth = title.size.width + icon.size.width + spacing
let size = CGSize(width: totalWidth + padding * 2.0, height: 28.0)
let size = CGSize(width: totalWidth + leftPadding + padding, height: 28.0)
let background = background.update(
component: RoundedRectangle(
color: component.backgroundColor,
@ -319,12 +334,21 @@ private final class ItemComponent: CombinedComponent {
context.add(background
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0))
)
context.add(title
.position(CGPoint(x: padding + title.size.width / 2.0, y: size.height / 2.0))
)
context.add(icon
.position(CGPoint(x: size.width - padding - icon.size.width / 2.0, y: size.height / 2.0))
)
if let _ = component.iconName {
context.add(title
.position(CGPoint(x: size.width - padding - title.size.width / 2.0, y: size.height / 2.0))
)
context.add(icon
.position(CGPoint(x: leftPadding + icon.size.width / 2.0, y: size.height / 2.0))
)
} else {
context.add(title
.position(CGPoint(x: padding + title.size.width / 2.0, y: size.height / 2.0))
)
context.add(icon
.position(CGPoint(x: size.width - padding - icon.size.width / 2.0, y: size.height / 2.0))
)
}
return size
}
}

View File

@ -0,0 +1,509 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import TelegramPresentationData
import ContextUI
final class GiftAttributeListContextItem: ContextMenuCustomItem {
let context: AccountContext
let attributes: [StarGift.UniqueGift.Attribute]
let selectedAttributes: [ResaleGiftsContext.Attribute]
let attributeCount: [ResaleGiftsContext.Attribute: Int32]
let searchQuery: Signal<String, NoError>
let attributeSelected: (ResaleGiftsContext.Attribute, Bool) -> Void
let selectAll: () -> Void
init(
context: AccountContext,
attributes: [StarGift.UniqueGift.Attribute],
selectedAttributes: [ResaleGiftsContext.Attribute],
attributeCount: [ResaleGiftsContext.Attribute: Int32],
searchQuery: Signal<String, NoError>,
attributeSelected: @escaping (ResaleGiftsContext.Attribute, Bool) -> Void,
selectAll: @escaping () -> Void
) {
self.context = context
self.attributes = attributes
self.selectedAttributes = selectedAttributes
self.attributeCount = attributeCount
self.searchQuery = searchQuery
self.attributeSelected = attributeSelected
self.selectAll = selectAll
}
func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
return GiftAttributeListContextItemNode(
presentationData: presentationData,
item: self,
getController: getController,
actionSelected: actionSelected
)
}
}
private func actionForAttribute(attribute: StarGift.UniqueGift.Attribute, presentationData: PresentationData, selectedAttributes: Set<ResaleGiftsContext.Attribute>, searchQuery: String, item: GiftAttributeListContextItem, getController: @escaping () -> ContextControllerProtocol?) -> ContextMenuActionItem? {
let searchComponents = searchQuery.lowercased().components(separatedBy: .whitespaces).filter { !$0.isEmpty }
switch attribute {
case let .model(name, file, _), let .pattern(name, file, _):
let attributeId: ResaleGiftsContext.Attribute
if case .model = attribute {
attributeId = .model(file.fileId.id)
} else {
attributeId = .pattern(file.fileId.id)
}
let isSelected = selectedAttributes.isEmpty || selectedAttributes.contains(attributeId)
var entities: [MessageTextEntity] = []
var entityFiles: [Int64: TelegramMediaFile] = [:]
entities = [
MessageTextEntity(
range: 0..<1,
type: .CustomEmoji(stickerPack: nil, fileId: file.fileId.id)
)
]
entityFiles[file.fileId.id] = file
var title = "# \(name)"
var count = ""
if let counter = item.attributeCount[.model(file.fileId.id)] {
count = " \(presentationStringsFormattedNumber(counter, presentationData.dateTimeFormat.groupingSeparator))"
entities.append(
MessageTextEntity(
range: title.count ..< title.count + count.count,
type: .Italic
)
)
title += count
}
let words = title.components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty }
var wordStartIndices: [String.Index] = []
var currentIndex = title.startIndex
for word in words {
while currentIndex < title.endIndex {
let range = title.range(of: word, range: currentIndex..<title.endIndex)
if let range = range {
wordStartIndices.append(range.lowerBound)
currentIndex = range.upperBound
break
}
currentIndex = title.index(after: currentIndex)
}
}
for (wordIndex, word) in words.enumerated() {
let lowercaseWord = word.lowercased()
for component in searchComponents {
if lowercaseWord.hasPrefix(component) {
let startIndex = wordStartIndices[wordIndex]
let prefixRange = startIndex..<title.index(startIndex, offsetBy: min(component.count, word.count))
entities.append(
MessageTextEntity(
range: title.distance(from: title.startIndex, to: prefixRange.lowerBound)..<title.distance(from: title.startIndex, to: prefixRange.upperBound),
type: .Bold
)
)
}
}
}
return ContextMenuActionItem(text: title, entities: entities, entityFiles: entityFiles, enableEntityAnimations: false, parseMarkdown: true, icon: { theme in
return isSelected ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, f in
getController()?.dismiss(result: .dismissWithoutContent, completion: nil)
item.attributeSelected(attributeId, false)
}, longPressAction: { _, f in
getController()?.dismiss(result: .dismissWithoutContent, completion: nil)
item.attributeSelected(attributeId, true)
})
case let .backdrop(name, id, innerColor, outerColor, _, _, _):
let attributeId: ResaleGiftsContext.Attribute = .backdrop(id)
let isSelected = selectedAttributes.isEmpty || selectedAttributes.contains(attributeId)
var entities: [MessageTextEntity] = []
var title = " \(name)"
var count = ""
if let counter = item.attributeCount[attributeId] {
count = " \(presentationStringsFormattedNumber(counter, presentationData.dateTimeFormat.groupingSeparator))"
entities.append(
MessageTextEntity(range: title.count ..< title.count + count.count, type: .Italic)
)
title += count
}
let words = title.components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty }
var wordStartIndices: [String.Index] = []
var currentIndex = title.startIndex
for word in words {
while currentIndex < title.endIndex {
let range = title.range(of: word, range: currentIndex..<title.endIndex)
if let range = range {
wordStartIndices.append(range.lowerBound)
currentIndex = range.upperBound
break
}
currentIndex = title.index(after: currentIndex)
}
}
for (wordIndex, word) in words.enumerated() {
let lowercaseWord = word.lowercased()
for component in searchComponents {
if lowercaseWord.hasPrefix(component) {
let startIndex = wordStartIndices[wordIndex]
let prefixRange = startIndex..<title.index(startIndex, offsetBy: min(component.count, word.count))
entities.append(
MessageTextEntity(
range: title.distance(from: title.startIndex, to: prefixRange.lowerBound)..<title.distance(from: title.startIndex, to: prefixRange.upperBound),
type: .Bold
)
)
}
}
}
return ContextMenuActionItem(text: title, entities: entities, icon: { theme in
return isSelected ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, additionalLeftIcon: { _ in
return generateGradientFilledCircleImage(diameter: 24.0, colors: [UIColor(rgb: UInt32(bitPattern: innerColor)).cgColor, UIColor(rgb: UInt32(bitPattern: outerColor)).cgColor])
}, action: { _, f in
getController()?.dismiss(result: .dismissWithoutContent, completion: nil)
item.attributeSelected(attributeId, false)
}, longPressAction: { _, f in
getController()?.dismiss(result: .dismissWithoutContent, completion: nil)
item.attributeSelected(attributeId, true)
})
default:
return nil
}
}
private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenuCustomNode, ContextActionNodeProtocol, ASScrollViewDelegate {
private let item: GiftAttributeListContextItem
private let presentationData: PresentationData
private let getController: () -> ContextControllerProtocol?
private let actionSelected: (ContextMenuActionResult) -> Void
private let scrollNode: ASScrollNode
private let actionNodes: [ContextControllerActionsListActionItemNode]
private let separatorNodes: [ASDisplayNode]
private var searchDisposable: Disposable?
private var searchQuery = ""
init(presentationData: PresentationData, item: GiftAttributeListContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
self.item = item
self.presentationData = presentationData
self.getController = getController
self.actionSelected = actionSelected
self.scrollNode = ASScrollNode()
var actionNodes: [ContextControllerActionsListActionItemNode] = []
var separatorNodes: [ASDisplayNode] = []
let selectedAttributes = Set(item.selectedAttributes)
//TODO:localize
let selectAllAction = ContextMenuActionItem(text: "Select All", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor)
}, iconPosition: .left, action: { _, f in
getController()?.dismiss(result: .dismissWithoutContent, completion: nil)
item.selectAll()
})
let selectAllActionNode = ContextControllerActionsListActionItemNode(context: item.context, getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: selectAllAction)
actionNodes.append(selectAllActionNode)
let separatorNode = ASDisplayNode()
separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
separatorNodes.append(separatorNode)
for attribute in item.attributes {
guard let action = actionForAttribute(attribute: attribute, presentationData: presentationData, selectedAttributes: selectedAttributes, searchQuery: self.searchQuery, item: item, getController: getController) else {
continue
}
let actionNode = ContextControllerActionsListActionItemNode(context: item.context, getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: action)
actionNodes.append(actionNode)
if actionNodes.count != item.attributes.count {
let separatorNode = ASDisplayNode()
separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
separatorNodes.append(separatorNode)
}
}
let nopAction: ((ContextControllerProtocol?, @escaping (ContextMenuActionResult) -> Void) -> Void)? = nil
let emptyResultsAction = ContextMenuActionItem(text: "No Results", textFont: .small, icon: { _ in return nil }, action: nopAction)
let emptyResultsActionNode = ContextControllerActionsListActionItemNode(context: item.context, getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: emptyResultsAction)
actionNodes.append(emptyResultsActionNode)
self.actionNodes = actionNodes
self.separatorNodes = separatorNodes
super.init()
self.addSubnode(self.scrollNode)
for separatorNode in self.separatorNodes {
self.scrollNode.addSubnode(separatorNode)
}
for actionNode in self.actionNodes {
self.scrollNode.addSubnode(actionNode)
}
self.searchDisposable = (item.searchQuery
|> deliverOnMainQueue).start(next: { [weak self] searchQuery in
guard let self, self.searchQuery != searchQuery else {
return
}
self.searchQuery = searchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
var i = 1
for attribute in item.attributes {
guard let action = actionForAttribute(attribute: attribute, presentationData: presentationData, selectedAttributes: selectedAttributes, searchQuery: self.searchQuery, item: item, getController: getController) else {
continue
}
self.actionNodes[i].setItem(item: action)
i += 1
}
self.getController()?.requestLayout(transition: .immediate)
})
}
deinit {
self.searchDisposable?.dispose()
}
override func didLoad() {
super.didLoad()
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.scrollNode.view.alwaysBounceVertical = false
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 5.0, right: 0.0)
}
func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
let minActionsWidth: CGFloat = 250.0
let maxActionsWidth: CGFloat = 300.0
let constrainedWidth = min(constrainedWidth, maxActionsWidth)
var maxWidth: CGFloat = 0.0
var contentHeight: CGFloat = 0.0
var heightsAndCompletions: [(Int, CGFloat, (CGSize, ContainedViewLayoutTransition) -> Void)] = []
let effectiveAttributes: [StarGift.UniqueGift.Attribute]
if self.searchQuery.isEmpty {
effectiveAttributes = self.item.attributes
} else {
effectiveAttributes = filteredAttributes(attributes: self.item.attributes, query: self.searchQuery)
}
let visibleAttributes = Set(effectiveAttributes.map { attribute -> AnyHashable in
switch attribute {
case let .model(_, file, _):
return file.fileId.id
case let .pattern(_, file, _):
return file.fileId.id
case let .backdrop(_, id, _, _, _, _, _):
return id
default:
fatalError()
}
})
for i in 0 ..< self.actionNodes.count {
let itemNode = self.actionNodes[i]
if !self.searchQuery.isEmpty && i == 0 {
itemNode.isHidden = true
continue
}
if i > 0 && i < self.actionNodes.count - 1 {
let attribute = self.item.attributes[i - 1]
let attributeId: AnyHashable
switch attribute {
case let .model(_, file, _):
attributeId = AnyHashable(file.fileId.id)
case let .pattern(_, file, _):
attributeId = AnyHashable(file.fileId.id)
case let .backdrop(_, id, _, _, _, _, _):
attributeId = AnyHashable(id)
default:
fatalError()
}
if !visibleAttributes.contains(attributeId) {
itemNode.isHidden = true
continue
}
}
if i == self.actionNodes.count - 1 {
if !visibleAttributes.isEmpty {
itemNode.isHidden = true
continue
} else {
}
}
itemNode.isHidden = false
let (minSize, complete) = itemNode.update(presentationData: self.presentationData, constrainedSize: CGSize(width: constrainedWidth, height: constrainedHeight))
maxWidth = max(maxWidth, minSize.width)
heightsAndCompletions.append((i, minSize.height, complete))
contentHeight += minSize.height
}
maxWidth = max(maxWidth, minActionsWidth)
let maxHeight: CGFloat = min(360.0, constrainedHeight - 108.0)
return (CGSize(width: maxWidth, height: min(maxHeight, contentHeight)), { size, transition in
var verticalOffset: CGFloat = 0.0
for (i, itemHeight, itemCompletion) in heightsAndCompletions {
let itemNode = self.actionNodes[i]
let itemSize = CGSize(width: maxWidth, height: itemHeight)
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: itemSize))
itemCompletion(itemSize, transition)
verticalOffset += itemHeight
if i < self.actionNodes.count - 2 {
let separatorNode = self.separatorNodes[i]
separatorNode.frame = CGRect(x: 0, y: verticalOffset, width: size.width, height: UIScreenPixel)
}
}
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
self.scrollNode.view.contentSize = CGSize(width: size.width, height: contentHeight)
})
}
func updateTheme(presentationData: PresentationData) {
}
var isActionEnabled: Bool {
return true
}
func performAction() {
}
func setIsHighlighted(_ value: Bool) {
}
func canBeHighlighted() -> Bool {
return self.isActionEnabled
}
func updateIsHighlighted(isHighlighted: Bool) {
self.setIsHighlighted(isHighlighted)
}
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol {
// for actionNode in self.actionNodes {
// let frame = actionNode.convert(actionNode.bounds, to: self)
// if frame.contains(point) {
// return actionNode
// }
// }
return self
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
for actionNode in self.actionNodes {
actionNode.updateIsHighlighted(isHighlighted: false)
}
}
}
private func stringTokens(_ string: String) -> [ValueBoxKey] {
let nsString = string.folding(options: .diacriticInsensitive, locale: .current).lowercased() as NSString
let flag = UInt(kCFStringTokenizerUnitWord)
let tokenizer = CFStringTokenizerCreate(kCFAllocatorDefault, nsString, CFRangeMake(0, nsString.length), flag, CFLocaleCopyCurrent())
var tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer)
var tokens: [ValueBoxKey] = []
var addedTokens = Set<ValueBoxKey>()
while tokenType != [] {
let currentTokenRange = CFStringTokenizerGetCurrentTokenRange(tokenizer)
if currentTokenRange.location >= 0 && currentTokenRange.length != 0 {
let token = ValueBoxKey(length: currentTokenRange.length * 2)
nsString.getCharacters(token.memory.assumingMemoryBound(to: unichar.self), range: NSMakeRange(currentTokenRange.location, currentTokenRange.length))
if !addedTokens.contains(token) {
tokens.append(token)
addedTokens.insert(token)
}
}
tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer)
}
return tokens
}
private func matchStringTokens(_ tokens: [ValueBoxKey], with other: [ValueBoxKey]) -> Bool {
if other.isEmpty {
return false
} else if other.count == 1 {
let otherToken = other[0]
for token in tokens {
if otherToken.isPrefix(to: token) {
return true
}
}
} else {
for otherToken in other {
var found = false
for token in tokens {
if otherToken.isPrefix(to: token) {
found = true
break
}
}
if !found {
return false
}
}
return true
}
return false
}
private func filteredAttributes(attributes: [StarGift.UniqueGift.Attribute], query: String) -> [StarGift.UniqueGift.Attribute] {
let queryTokens = stringTokens(query.lowercased())
var result: [StarGift.UniqueGift.Attribute] = []
for attribute in attributes {
let string: String
switch attribute {
case let .model(name, _, _):
string = name
case let .pattern(name, _, _):
string = name
case let .backdrop(name, _, _, _, _, _, _):
string = name
default:
continue
}
let tokens = stringTokens(string)
if matchStringTokens(tokens, with: queryTokens) {
result.append(attribute)
}
}
return result
}

View File

@ -88,7 +88,6 @@ final class GiftStoreScreenComponent: Component {
private var starsItems: [AnyHashable: ComponentView<Empty>] = [:]
private let filterSelector = ComponentView<Empty>()
private var isLoading = false
private var isUpdating: Bool = false
@ -144,48 +143,13 @@ final class GiftStoreScreenComponent: Component {
private var effectiveGifts: [StarGift]? {
if let gifts = self.state?.starGiftsState?.gifts {
return gifts
// if self.selectedModels.isEmpty && self.selectedBackdrops.isEmpty && self.selectedSymbols.isEmpty {
// return gifts
// } else if let (currentGifts, currentModels, currentBackdrops, currentSymbols) = self.currentGifts, currentModels == self.selectedModels && currentBackdrops == self.selectedBackdrops && currentSymbols == self.selectedSymbols {
// return currentGifts
// } else {
// var filteredGifts: [StarGift] = []
// for gift in gifts {
// guard case let .unique(uniqueGift) = gift else {
// continue
// }
// var match = true
// for attribute in uniqueGift.attributes {
// if case let .model(name, _, _) = attribute {
// if !self.selectedModels.isEmpty && !self.selectedModels.contains(name) {
// match = false
// }
// }
// if case let .backdrop(name, _, _, _, _, _, _) = attribute {
// if !self.selectedBackdrops.isEmpty && !self.selectedBackdrops.contains(name) {
// match = false
// }
// }
// if case let .pattern(name, _, _) = attribute {
// if !self.selectedSymbols.isEmpty && !self.selectedSymbols.contains(name) {
// match = false
// }
// }
// }
// if match {
// filteredGifts.append(gift)
// }
// }
// self.currentGifts = (filteredGifts, self.selectedModels, self.selectedBackdrops, self.selectedSymbols)
// return filteredGifts
// }
} else {
return nil
}
}
private func updateScrolling(interactive: Bool = false, transition: ComponentTransition) {
guard let environment = self.environment, let component = self.component, !self.isLoading else {
guard let environment = self.environment, let component = self.component, self.state?.starGiftsState?.dataState != .loading else {
return
}
@ -267,7 +231,7 @@ final class GiftStoreScreenComponent: Component {
),
effectAlignment: .center,
action: { [weak self] in
if let self, let component = self.component {
if let self, let component = self.component, let state = self.state {
if let controller = controller() as? GiftStoreScreen {
let mainController: ViewController
if let parentController = controller.parentController() {
@ -277,7 +241,7 @@ final class GiftStoreScreenComponent: Component {
}
let giftController = GiftViewScreen(
context: component.context,
subject: .uniqueGift(uniqueGift)
subject: .uniqueGift(uniqueGift, state.peerId)
)
mainController.push(giftController)
}
@ -330,77 +294,6 @@ final class GiftStoreScreenComponent: Component {
}
}
var selectedModels = Set<String>()
var selectedBackdrops = Set<String>()
var selectedSymbols = Set<String>()
private func simulateLoading() {
self.isLoading = true
self.state?.updated(transition: .immediate)
Queue.mainQueue().after(1.0, {
self.isLoading = false
self.state?.updated(transition: .immediate)
})
}
func openContextMenu(sourceView: UIView) {
guard let component = self.component, let controller = self.environment?.controller() else {
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: "Sort by Price", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortValue"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
self?.state?.starGiftsContext.updateSorting(.value)
})))
items.append(.action(ContextMenuActionItem(text: "Sort by Date", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortDate"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
self?.state?.starGiftsContext.updateSorting(.date)
})))
items.append(.action(ContextMenuActionItem(text: "Sort by Number", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortNumber"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
self?.state?.starGiftsContext.updateSorting(.number)
})))
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: "Model", textLayout: .secondLineWithValue("all models"), icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortNumber"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.default)
})))
items.append(.action(ContextMenuActionItem(text: "Backdrop", textLayout: .secondLineWithValue("all backdrops"), icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortNumber"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.default)
})))
items.append(.action(ContextMenuActionItem(text: "Symbol", textLayout: .secondLineWithValue("all symbols"), icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortNumber"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.default)
})))
let contextController = ContextController(context: component.context, presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
controller.presentInGlobalOverlay(contextController)
}
func openSortContextMenu(sourceView: UIView) {
guard let component = self.component, let controller = self.environment?.controller() else {
return
@ -441,28 +334,70 @@ final class GiftStoreScreenComponent: Component {
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let searchQueryPromise = ValuePromise<String>("")
var items: [ContextMenuItem] = []
var allSelected = true
var currentFilterAttributes: [ResaleGiftsContext.Attribute] = []
var selectedIds = Set<Int64>()
if let filterAttributes = self.state?.starGiftsState?.filterAttributes {
currentFilterAttributes = filterAttributes
for attribute in filterAttributes {
if case let .model(id) = attribute {
allSelected = false
selectedIds.insert(id)
}
let attributes = self.state?.starGiftsState?.attributes ?? []
let modelAttributes = attributes.filter { attribute in
if case .model = attribute {
return true
} else {
return false
}
}
items.append(.action(ContextMenuActionItem(text: "Select All", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor)
}, iconPosition: .left, action: { [weak self] _, f in
f(.default)
if let self {
let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? []
let selectedModelAttributes = currentFilterAttributes.filter { attribute in
if case .model = attribute {
return true
} else {
return false
}
}
//TODO:localize
var items: [ContextMenuItem] = []
items.append(.custom(SearchContextItem(
context: component.context,
placeholder: "Search",
value: "",
valueChanged: { value in
searchQueryPromise.set(value)
}
), false))
items.append(.separator)
items.append(.custom(GiftAttributeListContextItem(
context: component.context,
attributes: modelAttributes,
selectedAttributes: selectedModelAttributes,
attributeCount: self.state?.starGiftsState?.attributeCount ?? [:],
searchQuery: searchQueryPromise.get(),
attributeSelected: { [weak self] attribute, exclusive in
guard let self else {
return
}
var updatedFilterAttributes: [ResaleGiftsContext.Attribute]
if exclusive {
updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .model = attribute {
return false
}
return true
}
updatedFilterAttributes.append(attribute)
} else {
updatedFilterAttributes = currentFilterAttributes
if selectedModelAttributes.contains(attribute) {
updatedFilterAttributes.removeAll(where: { $0 == attribute })
} else {
updatedFilterAttributes.append(attribute)
}
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
},
selectAll: { [weak self] in
guard let self else {
return
}
let updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .model = attribute {
return false
@ -471,65 +406,15 @@ final class GiftStoreScreenComponent: Component {
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
}
})))
), false))
if let attributes = self.state?.starGiftsState?.attributes {
for attribute in attributes {
if case let .model(name, file, _) = attribute {
let isSelected = allSelected || selectedIds.contains(file.fileId.id)
var entities: [MessageTextEntity] = []
var entityFiles: [Int64: TelegramMediaFile] = [:]
entities = [
MessageTextEntity(
range: 0..<1,
type: .CustomEmoji(stickerPack: nil, fileId: file.fileId.id)
)
]
entityFiles[file.fileId.id] = file
var title = "# \(name)"
var count = ""
if let counter = self.state?.starGiftsState?.attributeCount[.model(file.fileId.id)] {
count = " \(presentationStringsFormattedNumber(counter, presentationData.dateTimeFormat.groupingSeparator))"
entities.append(
MessageTextEntity(range: title.count ..< title.count + count.count, type: .Bold)
)
title += count
}
items.append(.action(ContextMenuActionItem(text: title, entities: entities, entityFiles: entityFiles, enableEntityAnimations: false, parseMarkdown: true, icon: { theme in
return isSelected ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { [weak self] _, f in
f(.default)
if let self {
var updatedFilterAttributes = currentFilterAttributes
if selectedIds.contains(file.fileId.id) {
updatedFilterAttributes.removeAll(where: { $0 == .model(file.fileId.id) })
} else {
updatedFilterAttributes.append(.model(file.fileId.id))
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
}
}, longPressAction: { [weak self] _, f in
f(.default)
if let self {
var updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .model = attribute {
return false
}
return true
}
updatedFilterAttributes.append(.model(file.fileId.id))
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
}
})))
}
}
}
let contextController = ContextController(context: component.context, presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
let contextController = ContextController(
context: component.context,
presentationData: presentationData,
source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)),
items: .single(ContextController.Items(content: .list(items))),
gesture: nil
)
controller.presentInGlobalOverlay(contextController)
}
@ -539,28 +424,70 @@ final class GiftStoreScreenComponent: Component {
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let searchQueryPromise = ValuePromise<String>("")
var items: [ContextMenuItem] = []
var allSelected = true
var currentFilterAttributes: [ResaleGiftsContext.Attribute] = []
var selectedIds = Set<Int32>()
if let filterAttributes = self.state?.starGiftsState?.filterAttributes {
currentFilterAttributes = filterAttributes
for attribute in filterAttributes {
if case let .backdrop(id) = attribute {
allSelected = false
selectedIds.insert(id)
}
let attributes = self.state?.starGiftsState?.attributes ?? []
let backdropAttributes = attributes.filter { attribute in
if case .backdrop = attribute {
return true
} else {
return false
}
}
items.append(.action(ContextMenuActionItem(text: "Select All", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor)
}, iconPosition: .left, action: { [weak self] _, f in
f(.default)
if let self {
let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? []
let selectedBackdropAttributes = currentFilterAttributes.filter { attribute in
if case .backdrop = attribute {
return true
} else {
return false
}
}
//TODO:localize
var items: [ContextMenuItem] = []
items.append(.custom(SearchContextItem(
context: component.context,
placeholder: "Search",
value: "",
valueChanged: { value in
searchQueryPromise.set(value)
}
), false))
items.append(.separator)
items.append(.custom(GiftAttributeListContextItem(
context: component.context,
attributes: backdropAttributes,
selectedAttributes: selectedBackdropAttributes,
attributeCount: self.state?.starGiftsState?.attributeCount ?? [:],
searchQuery: searchQueryPromise.get(),
attributeSelected: { [weak self] attribute, exclusive in
guard let self else {
return
}
var updatedFilterAttributes: [ResaleGiftsContext.Attribute]
if exclusive {
updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .backdrop = attribute {
return false
}
return true
}
updatedFilterAttributes.append(attribute)
} else {
updatedFilterAttributes = currentFilterAttributes
if selectedBackdropAttributes.contains(attribute) {
updatedFilterAttributes.removeAll(where: { $0 == attribute })
} else {
updatedFilterAttributes.append(attribute)
}
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
},
selectAll: { [weak self] in
guard let self else {
return
}
let updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .backdrop = attribute {
return false
@ -569,58 +496,15 @@ final class GiftStoreScreenComponent: Component {
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
}
})))
), false))
if let attributes = self.state?.starGiftsState?.attributes {
for attribute in attributes {
if case let .backdrop(name, id, innerColor, outerColor, _, _, _) = attribute {
let isSelected = allSelected || selectedIds.contains(id)
var entities: [MessageTextEntity] = []
var title = "\(name)"
var count = ""
if let counter = self.state?.starGiftsState?.attributeCount[.backdrop(id)] {
count = " \(presentationStringsFormattedNumber(counter, presentationData.dateTimeFormat.groupingSeparator))"
entities.append(
MessageTextEntity(range: title.count ..< title.count + count.count, type: .Bold)
)
title += count
}
items.append(.action(ContextMenuActionItem(text: "\(name)\(count)", entities: entities, icon: { theme in
return isSelected ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, additionalLeftIcon: { _ in
return generateGradientFilledCircleImage(diameter: 24.0, colors: [UIColor(rgb: UInt32(bitPattern: innerColor)).cgColor, UIColor(rgb: UInt32(bitPattern: outerColor)).cgColor])
}, action: { [weak self] _, f in
f(.default)
if let self {
var updatedFilterAttributes = currentFilterAttributes
if selectedIds.contains(id) {
updatedFilterAttributes.removeAll(where: { $0 == .backdrop(id) })
} else {
updatedFilterAttributes.append(.backdrop(id))
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
}
}, longPressAction: { [weak self] _, f in
f(.default)
if let self {
var updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .backdrop = attribute {
return false
}
return true
}
updatedFilterAttributes.append(.backdrop(id))
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
}
})))
}
}
}
let contextController = ContextController(context: component.context, presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
let contextController = ContextController(
context: component.context,
presentationData: presentationData,
source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)),
items: .single(ContextController.Items(content: .list(items))),
gesture: nil
)
controller.presentInGlobalOverlay(contextController)
}
@ -630,28 +514,70 @@ final class GiftStoreScreenComponent: Component {
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let searchQueryPromise = ValuePromise<String>("")
var items: [ContextMenuItem] = []
var allSelected = true
var currentFilterAttributes: [ResaleGiftsContext.Attribute] = []
var selectedIds = Set<Int64>()
if let filterAttributes = self.state?.starGiftsState?.filterAttributes {
currentFilterAttributes = filterAttributes
for attribute in filterAttributes {
if case let .pattern(id) = attribute {
allSelected = false
selectedIds.insert(id)
}
let attributes = self.state?.starGiftsState?.attributes ?? []
let patternAttributes = attributes.filter { attribute in
if case .pattern = attribute {
return true
} else {
return false
}
}
items.append(.action(ContextMenuActionItem(text: "Select All", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor)
}, iconPosition: .left, action: { [weak self] _, f in
f(.default)
if let self {
let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? []
let selectedPatternAttributes = currentFilterAttributes.filter { attribute in
if case .pattern = attribute {
return true
} else {
return false
}
}
//TODO:localize
var items: [ContextMenuItem] = []
items.append(.custom(SearchContextItem(
context: component.context,
placeholder: "Search",
value: "",
valueChanged: { value in
searchQueryPromise.set(value)
}
), false))
items.append(.separator)
items.append(.custom(GiftAttributeListContextItem(
context: component.context,
attributes: patternAttributes,
selectedAttributes: selectedPatternAttributes,
attributeCount: self.state?.starGiftsState?.attributeCount ?? [:],
searchQuery: searchQueryPromise.get(),
attributeSelected: { [weak self] attribute, exclusive in
guard let self else {
return
}
var updatedFilterAttributes: [ResaleGiftsContext.Attribute]
if exclusive {
updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .pattern = attribute {
return false
}
return true
}
updatedFilterAttributes.append(attribute)
} else {
updatedFilterAttributes = currentFilterAttributes
if selectedPatternAttributes.contains(attribute) {
updatedFilterAttributes.removeAll(where: { $0 == attribute })
} else {
updatedFilterAttributes.append(attribute)
}
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
},
selectAll: { [weak self] in
guard let self else {
return
}
let updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .pattern = attribute {
return false
@ -660,65 +586,15 @@ final class GiftStoreScreenComponent: Component {
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
}
})))
), false))
if let attributes = self.state?.starGiftsState?.attributes {
for attribute in attributes {
if case let .pattern(name, file, _) = attribute {
let isSelected = allSelected || selectedIds.contains(file.fileId.id)
var entities: [MessageTextEntity] = []
var entityFiles: [Int64: TelegramMediaFile] = [:]
entities = [
MessageTextEntity(
range: 0..<1,
type: .CustomEmoji(stickerPack: nil, fileId: file.fileId.id)
)
]
entityFiles[file.fileId.id] = file
var title = "# \(name)"
var count = ""
if let counter = self.state?.starGiftsState?.attributeCount[.pattern(file.fileId.id)] {
count = " \(presentationStringsFormattedNumber(counter, presentationData.dateTimeFormat.groupingSeparator))"
entities.append(
MessageTextEntity(range: title.count ..< title.count + count.count, type: .Bold)
)
title += count
}
items.append(.action(ContextMenuActionItem(text: title, entities: entities, entityFiles: entityFiles, enableEntityAnimations: false, icon: { theme in
return isSelected ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { [weak self] _, f in
f(.default)
if let self {
var updatedFilterAttributes = currentFilterAttributes
if selectedIds.contains(file.fileId.id) {
updatedFilterAttributes.removeAll(where: { $0 == .pattern(file.fileId.id) })
} else {
updatedFilterAttributes.append(.pattern(file.fileId.id))
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
}
}, longPressAction: { [weak self] _, f in
f(.default)
if let self {
var updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .pattern = attribute {
return false
}
return true
}
updatedFilterAttributes.append(.pattern(file.fileId.id))
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
}
})))
}
}
}
let contextController = ContextController(context: component.context, presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
let contextController = ContextController(
context: component.context,
presentationData: presentationData,
source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)),
items: .single(ContextController.Items(content: .list(items))),
gesture: nil
)
controller.presentInGlobalOverlay(contextController)
}
@ -729,7 +605,6 @@ final class GiftStoreScreenComponent: Component {
}
let environment = environment[EnvironmentType.self].value
let controller = environment.controller
let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment
self.state = state
@ -785,6 +660,9 @@ final class GiftStoreScreenComponent: Component {
let topSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelSize.height), size: CGSize(width: topSeparatorSize.width, height: topSeparatorSize.height))
if let topPanelView = self.topPanel.view, let topSeparatorView = self.topSeparator.view {
if topPanelView.superview == nil {
topPanelView.alpha = 0.0
topSeparatorView.alpha = 0.0
self.addSubview(topPanelView)
self.addSubview(topSeparatorView)
}
@ -792,51 +670,19 @@ final class GiftStoreScreenComponent: Component {
transition.setFrame(view: topSeparatorView, frame: topSeparatorFrame)
}
let cancelButtonSize = self.cancelButton.update(
transition: transition,
component: AnyComponent(
PlainButtonComponent(
content: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor)),
horizontalAlignment: .center
)
),
effectAlignment: .center,
action: {
controller()?.dismiss()
},
animateScale: false
)
),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
let cancelButtonFrame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 16.0, y: environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0 - cancelButtonSize.height / 2.0), size: cancelButtonSize)
if let cancelButtonView = self.cancelButton.view {
if cancelButtonView.superview == nil {
self.addSubview(cancelButtonView)
}
transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame)
}
// let showFilters = !"".isEmpty
// let sortButtonSize = self.sortButton.update(
// let cancelButtonSize = self.cancelButton.update(
// transition: transition,
// component: AnyComponent(
// PlainButtonComponent(
// content: AnyComponent(
// BundleIconComponent(
// name: "Peer Info/SortIcon",
// tintColor: theme.rootController.navigationBar.accentTextColor
// MultilineTextComponent(
// text: .plain(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor)),
// horizontalAlignment: .center
// )
// ),
// effectAlignment: .center,
// action: { [weak self] in
// if let sourceView = self?.sortButton.view {
// self?.openContextMenu(sourceView: sourceView)
// }
// action: {
// controller()?.dismiss()
// },
// animateScale: false
// )
@ -844,15 +690,14 @@ final class GiftStoreScreenComponent: Component {
// environment: {},
// containerSize: CGSize(width: availableSize.width, height: 100.0)
// )
// let sortButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - environment.safeInsets.right - sortButtonSize.width - 10.0, y: environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0 - sortButtonSize.height / 2.0), size: sortButtonSize)
// if let sortButtonView = self.sortButton.view {
// if sortButtonView.superview == nil {
// self.addSubview(sortButtonView)
// let cancelButtonFrame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 16.0, y: environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0 - cancelButtonSize.height / 2.0), size: cancelButtonSize)
// if let cancelButtonView = self.cancelButton.view {
// if cancelButtonView.superview == nil {
// self.addSubview(cancelButtonView)
// }
// transition.setFrame(view: sortButtonView, frame: sortButtonFrame)
// transition.setAlpha(view: sortButtonView, alpha: showFilters ? 0.0 : 1.0)
// transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame)
// }
let balanceTitleSize = self.balanceTitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
@ -902,16 +747,12 @@ final class GiftStoreScreenComponent: Component {
balanceValueView.bounds = CGRect(origin: .zero, size: balanceValueSize)
balanceIconView.center = CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - balanceValueSize.width - balanceIconSize.width / 2.0 - 2.0, y: topBalanceOriginY + balanceTitleSize.height + balanceValueSize.height / 2.0 - UIScreenPixel)
balanceIconView.bounds = CGRect(origin: .zero, size: balanceIconSize)
// transition.setAlpha(view: balanceTitleView, alpha: showFilters ? 1.0 : 0.0)
// transition.setAlpha(view: balanceValueView, alpha: showFilters ? 1.0 : 0.0)
// transition.setAlpha(view: balanceIconView, alpha: showFilters ? 1.0 : 0.0)
}
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "Gift Name", font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)),
text: .plain(NSAttributedString(string: component.gift.title ?? "Gift", font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center
)),
environment: {},
@ -924,10 +765,19 @@ final class GiftStoreScreenComponent: Component {
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: 10.0), size: titleSize))
}
let effectiveCount: Int32
if let count = self.effectiveGifts?.count {
effectiveCount = Int32(count)
} else if let resale = component.gift.availability?.resale {
effectiveCount = Int32(resale)
} else {
effectiveCount = 0
}
let subtitleSize = self.subtitle.update(
transition: transition,
component: AnyComponent(BalancedTextComponent(
text: .plain(NSAttributedString(string: "\(self.effectiveGifts?.count ?? 0) for resale", font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)),
text: .plain(NSAttributedString(string: "\(effectiveCount) for resale", font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
)),
@ -946,20 +796,25 @@ final class GiftStoreScreenComponent: Component {
let optionWidth = (availableSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
var sortingTitle = "Date"
var sortingIcon: String = "Peer Info/SortDate"
if let sorting = self.state?.starGiftsState?.sorting {
switch sorting {
case .date:
sortingTitle = "Date"
sortingIcon = "Peer Info/SortDate"
case .value:
sortingTitle = "Price"
sortingIcon = "Peer Info/SortValue"
case .number:
sortingTitle = "Number"
sortingIcon = "Peer Info/SortNumber"
}
}
var filterItems: [FilterSelectorComponent.Item] = []
filterItems.append(FilterSelectorComponent.Item(
id: AnyHashable(0),
iconName: sortingIcon,
title: sortingTitle,
action: { [weak self] view in
if let self {
@ -1043,7 +898,7 @@ final class GiftStoreScreenComponent: Component {
component: AnyComponent(FilterSelectorComponent(
context: component.context,
colors: FilterSelectorComponent.Colors(
foreground: theme.list.itemSecondaryTextColor,
foreground: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.65),
background: theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15)
),
items: filterItems
@ -1081,7 +936,7 @@ final class GiftStoreScreenComponent: Component {
self.scrollView.contentSize = contentSize
self.nextScrollTransition = nil
}
let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
let scrollInsets = UIEdgeInsets(top: topPanelHeight, left: 0.0, bottom: 0.0, right: 0.0)
if self.scrollView.scrollIndicatorInsets != scrollInsets {
self.scrollView.scrollIndicatorInsets = scrollInsets
}
@ -1113,7 +968,7 @@ final class GiftStoreScreenComponent: Component {
transition.setFrame(view: self.loadingNode.view, frame: CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight + 39.0 + 7.0), size: availableSize))
let fadeTransition = ComponentTransition.easeInOut(duration: 0.25)
if let effectiveGifts = self.effectiveGifts, effectiveGifts.isEmpty && !self.isLoading {
if let effectiveGifts = self.effectiveGifts, effectiveGifts.isEmpty && self.state?.starGiftsState?.dataState != .loading {
let sideInset: CGFloat = 44.0
let emptyAnimationHeight = 148.0
let topInset: CGFloat = environment.navigationHeight + 39.0
@ -1149,10 +1004,7 @@ final class GiftStoreScreenComponent: Component {
guard let self else {
return
}
self.selectedModels.removeAll()
self.selectedBackdrops.removeAll()
self.selectedSymbols.removeAll()
self.simulateLoading()
self.state?.starGiftsContext.updateFilterAttributes([])
},
animateScale: false
)
@ -1205,6 +1057,8 @@ final class GiftStoreScreenComponent: Component {
}
view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size)
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsActionFrame.center)
view.alpha = self.state?.starGiftsState?.attributes.isEmpty == true ? 0.0 : 1.0
}
} else {
if let view = self.emptyResultsAnimation.view {
@ -1234,6 +1088,7 @@ final class GiftStoreScreenComponent: Component {
final class State: ComponentState {
private let context: AccountContext
var peerId: EnginePeer.Id
private var disposable: Disposable?
fileprivate let starGiftsContext: ResaleGiftsContext
@ -1241,9 +1096,11 @@ final class GiftStoreScreenComponent: Component {
init(
context: AccountContext,
peerId: EnginePeer.Id,
giftId: Int64
) {
self.context = context
self.peerId = peerId
self.starGiftsContext = ResaleGiftsContext(account: context.account, giftId: giftId)
super.init()
@ -1264,7 +1121,7 @@ final class GiftStoreScreenComponent: Component {
}
func makeState() -> State {
return State(context: self.context, giftId: self.gift.id)
return State(context: self.context, peerId: self.peerId, giftId: self.gift.id)
}
func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
@ -1292,11 +1149,10 @@ public class GiftStoreScreen: ViewControllerComponentContainer {
starsContext: starsContext,
peerId: peerId,
gift: gift
), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil)
), navigationBarAppearance: .transparent, theme: .default, updatedPresentationData: nil)
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.context.sharedContext.currentPresentationData.with { $0 }.strings.Common_Back, style: .plain, target: nil, action: nil)
self.scrollToTop = { [weak self] in
guard let self, let componentView = self.node.hostView.componentView as? GiftStoreScreenComponent.View else {
return
@ -1332,6 +1188,8 @@ private final class GiftStoreReferenceContentSource: ContextReferenceContentSour
private let controller: ViewController
private let sourceView: UIView
let forceDisplayBelowKeyboard = true
init(controller: ViewController, sourceView: UIView) {
self.controller = controller
self.sourceView = sourceView

View File

@ -134,6 +134,7 @@ final class LoadingShimmerNode: ASDisplayNode {
super.init()
self.allowsGroupOpacity = true
self.isUserInteractionEnabled = false
self.addSubnode(self.backgroundColorNode)

View File

@ -0,0 +1,223 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import TelegramPresentationData
import ContextUI
import TextFieldComponent
import MultilineTextComponent
import BundleIconComponent
final class SearchContextItem: ContextMenuCustomItem {
let context: AccountContext
let placeholder: String
let value: String
let valueChanged: (String) -> Void
init(
context: AccountContext,
placeholder: String,
value: String,
valueChanged: @escaping (String) -> Void
) {
self.context = context
self.placeholder = placeholder
self.value = value
self.valueChanged = valueChanged
}
func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
return SearchContextItemNode(
presentationData: presentationData,
item: self,
getController: getController,
actionSelected: actionSelected
)
}
}
private final class SearchContextItemNode: ASDisplayNode, ContextMenuCustomNode, ContextActionNodeProtocol, ASScrollViewDelegate {
private let item: SearchContextItem
private let presentationData: PresentationData
private let getController: () -> ContextControllerProtocol?
private let actionSelected: (ContextMenuActionResult) -> Void
private let state = EmptyComponentState()
private let icon = ComponentView<Empty>()
private let inputField = ComponentView<Empty>()
private let inputFieldExternalState = TextFieldComponent.ExternalState()
private let inputPlaceholderView = ComponentView<Empty>()
private let inputClear = ComponentView<Empty>()
private var inputText = ""
private var validLayout: CGSize?
init(presentationData: PresentationData, item: SearchContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
self.item = item
self.presentationData = presentationData
self.getController = getController
self.actionSelected = actionSelected
super.init()
self.state._updated = { [weak self] transition, _ in
guard let self, let size = self.validLayout else {
return
}
self.internalUpdateLayout(size: size, transition: transition)
}
}
func internalUpdateLayout(size: CGSize, transition: ComponentTransition) {
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Search", tintColor: self.presentationData.theme.contextMenu.primaryColor)),
environment: {},
containerSize: size
)
let iconFrame = CGRect(origin: CGPoint(x: 17.0, y: floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.view.addSubview(iconView)
}
transition.setFrame(view: iconView, frame: iconFrame)
}
let inputInset: CGFloat = 42.0
self.inputField.parentState = self.state
let inputFieldSize = self.inputField.update(
transition: .immediate,
component: AnyComponent(TextFieldComponent(
context: self.item.context,
theme: self.presentationData.theme,
strings: self.presentationData.strings,
externalState: self.inputFieldExternalState,
fontSize: self.presentationData.listsFontSize.baseDisplaySize,
textColor: self.presentationData.theme.contextMenu.primaryColor,
accentColor: self.presentationData.theme.contextMenu.primaryColor,
insets: UIEdgeInsets(top: 8.0, left: 2.0, bottom: 8.0, right: 2.0),
hideKeyboard: false,
customInputView: nil,
resetText: nil,
isOneLineWhenUnfocused: false,
emptyLineHandling: .notAllowed,
formatMenuAvailability: .none,
returnKeyType: .search,
lockedFormatAction: {
},
present: { _ in
},
paste: { _ in
},
returnKeyAction: nil,
backspaceKeyAction: nil
)),
environment: {},
containerSize: CGSize(width: size.width - inputInset - 40.0, height: size.height)
)
let inputFieldFrame = CGRect(origin: CGPoint(x: inputInset, y: floorToScreenPixels((size.height - inputFieldSize.height) / 2.0)), size: inputFieldSize)
if let inputFieldView = self.inputField.view as? TextFieldComponent.View {
if inputFieldView.superview == nil {
self.view.addSubview(inputFieldView)
}
transition.setFrame(view: inputFieldView, frame: inputFieldFrame)
}
if self.inputText != self.inputFieldExternalState.text.string {
self.inputText = self.inputFieldExternalState.text.string
self.item.valueChanged(self.inputText)
}
let inputPlaceholderSize = self.inputPlaceholderView.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(
string: self.item.placeholder,
font: Font.regular(self.presentationData.listsFontSize.baseDisplaySize),
textColor: self.presentationData.theme.contextMenu.secondaryColor
)))
),
environment: {},
containerSize: size
)
let inputPlaceholderFrame = CGRect(origin: CGPoint(x: inputInset + 10.0, y: floorToScreenPixels(inputFieldFrame.midY - inputPlaceholderSize.height / 2.0)), size: inputPlaceholderSize)
if let inputPlaceholderView = self.inputPlaceholderView.view {
if inputPlaceholderView.superview == nil {
inputPlaceholderView.isUserInteractionEnabled = false
self.view.addSubview(inputPlaceholderView)
}
inputPlaceholderView.frame = inputPlaceholderFrame
inputPlaceholderView.isHidden = self.inputFieldExternalState.hasText
}
let inputClearSize = self.inputClear.update(
transition: .immediate,
component: AnyComponent(
Button(
content: AnyComponent(
BundleIconComponent(name: "Components/Search Bar/Clear", tintColor: self.presentationData.theme.contextMenu.secondaryColor, maxSize: CGSize(width: 24.0, height: 24.0))
),
action: { [weak self] in
guard let self else {
return
}
if let inputFieldView = self.inputField.view as? TextFieldComponent.View {
inputFieldView.updateText(NSAttributedString(), selectionRange: 0..<0)
}
}
)
),
environment: {},
containerSize: CGSize(width: 30.0, height: 30.0)
)
let inputClearFrame = CGRect(origin: CGPoint(x: size.width - inputClearSize.width - 16.0, y: floorToScreenPixels(inputFieldFrame.midY - inputClearSize.height / 2.0)), size: inputClearSize)
if let inputClearView = self.inputClear.view {
if inputClearView.superview == nil {
self.view.addSubview(inputClearView)
}
inputClearView.frame = inputClearFrame
inputClearView.isHidden = !self.inputFieldExternalState.hasText
}
}
func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
let maxWidth: CGFloat = 220.0
let height: CGFloat = 42.0
return (CGSize(width: maxWidth, height: height), { size, transition in
self.validLayout = size
self.internalUpdateLayout(size: size, transition: ComponentTransition(transition))
})
}
func updateTheme(presentationData: PresentationData) {
}
var isActionEnabled: Bool {
return true
}
func performAction() {
}
func setIsHighlighted(_ value: Bool) {
}
func canBeHighlighted() -> Bool {
return false
}
func updateIsHighlighted(isHighlighted: Bool) {
}
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol {
return self
}
}

View File

@ -9,6 +9,95 @@ import MoreButtonNode
import AccountContext
import TelegramPresentationData
final class PriceButtonComponent: Component {
let price: Int64
init(
price: Int64
) {
self.price = price
}
static func ==(lhs: PriceButtonComponent, rhs: PriceButtonComponent) -> Bool {
return lhs.price == rhs.price
}
final class View: UIView {
private let backgroundView = UIView()
private let icon = UIImageView()
private let text = ComponentView<Empty>()
private var component: PriceButtonComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundView.clipsToBounds = true
self.addSubview(self.backgroundView)
self.icon.image = UIImage(bundleImageName: "Premium/Stars/ButtonStar")?.withRenderingMode(.alwaysTemplate)
self.backgroundView.addSubview(self.icon)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: PriceButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
var backgroundSize = CGSize(width: 42.0, height: 30.0)
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "\(component.price)",
font: Font.semibold(11.0),
textColor: UIColor(rgb: 0xffffff)
))
)),
environment: {},
containerSize: availableSize
)
let textFrame = CGRect(origin: CGPoint(x: 32.0, y: floorToScreenPixels((backgroundSize.height - textSize.height) / 2.0)), size: textSize)
if let textView = self.text.view {
if textView.superview == nil {
self.backgroundView.addSubview(textView)
}
transition.setFrame(view: textView, frame: textFrame)
}
backgroundSize.width += textSize.width
self.backgroundView.layer.cornerRadius = backgroundSize.height / 2.0
let backgroundColor: UIColor = UIColor(rgb: 0xffffff, alpha: 0.1)
transition.setBackgroundColor(view: self.backgroundView, color: backgroundColor)
let backgroundFrame = CGRect(origin: .zero, size: backgroundSize)
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
if let iconSize = self.icon.image?.size {
let iconFrame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((backgroundSize.height - iconSize.height) / 2.0)), size: iconSize)
transition.setFrame(view: self.icon, frame: iconFrame)
}
self.icon.tintColor = UIColor(rgb: 0xffffff)
return backgroundSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class ButtonsComponent: Component {
let theme: PresentationTheme
let isOverlay: Bool

View File

@ -2010,7 +2010,8 @@ final class MediaEditorScreenComponent: Component {
self.isSelectionPanelOpen = !self.isSelectionPanelOpen
self.state?.updated()
}
}
},
animateAlpha: false
)),
environment: {},
containerSize: CGSize(width: 33.0, height: 33.0)

View File

@ -4867,6 +4867,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
return profileGifts.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo)
},
buyGift: { [weak profileGifts] slug, peerId in
guard let profileGifts else {
return .never()
}
return profileGifts.buyStarGift(slug: slug, peerId: peerId)
},
shareStory: { [weak self] uniqueGift in
guard let self, let controller = self.controller else {
return
@ -11118,7 +11124,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
giftsContext?.updateSorting(sorting == .date ? .value : .date)
})))
if hasPinnedGifts {
if hasPinnedGifts && hasVisibility {
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Reorder, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor)
}, action: { _, f in

View File

@ -600,6 +600,12 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
return self.profileGifts.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo)
},
buyGift: { [weak self] slug, peerId in
guard let self else {
return .never()
}
return self.profileGifts.buyStarGift(slug: slug, peerId: peerId)
},
updateResellStars: { [weak self] price in
guard let self, case let .unique(uniqueGift) = product.gift else {
return

View File

@ -147,9 +147,9 @@ private final class SheetContent: CombinedComponent {
minAmount = StarsAmount(value: 1, nanos: 0)
maxAmount = withdrawConfiguration.maxPaidMediaAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
amountLabel = nil
case .starGiftResell:
case let .starGiftResell(update):
//TODO:localize
titleString = "Sell Gift"
titleString = update ? "Edit Price" : "Sell Gift"
amountTitle = "PRICE IN STARS"
amountPlaceholder = "Enter Price"
@ -358,12 +358,6 @@ private final class SheetContent: CombinedComponent {
buttonAttributedString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: buttonAttributedString.string))
}
// if let range = buttonAttributedString.string.range(of: "#"), let starImage = state.cachedStarImage?.0 {
// buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
// buttonAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: buttonAttributedString.string))
// buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string))
// }
let button = button.update(
component: ButtonComponent(
background: ButtonComponent.Background(
@ -558,10 +552,11 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
case accountWithdraw
case paidMedia(Int64?)
case reaction(Int64?)
case starGiftResell
case starGiftResell(Bool)
}
private let context: AccountContext
private let mode: StarsWithdrawScreen.Mode
fileprivate let completion: (Int64) -> Void
public init(
@ -570,6 +565,7 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
completion: @escaping (Int64) -> Void
) {
self.context = context
self.mode = mode
self.completion = completion
super.init(
@ -603,12 +599,17 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
func presentMinAmountTooltip(_ minAmount: Int64) {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
var text = presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum(presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum_Stars(Int32(minAmount))).string
if case .starGiftResell = self.mode {
text = "You cannot sell gift for less than \(presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum_Stars(Int32(minAmount)))."
}
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .image(
image: UIImage(bundleImageName: "Premium/Stars/StarLarge")!,
title: nil,
text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum(presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum_Stars(Int32(minAmount))).string,
text: text,
round: false,
undoText: nil
),

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "combine.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -2987,11 +2987,11 @@ public final class SharedAccountContextImpl: SharedAccountContext {
))
controller.navigationPresentation = .modal
let _ = combineLatest(
let _ = (combineLatest(
queue: Queue.mainQueue(),
controller.result,
options.get()
).startStandalone(next: { [weak controller] result, options in
options.get())
|> take(1)).startStandalone(next: { [weak controller] result, options in
if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer, let starsContext = context.starsContext {
if case .starGiftTransfer = source {
presentTransferAlertImpl?(EnginePeer(peer))
@ -3275,7 +3275,6 @@ public final class SharedAccountContextImpl: SharedAccountContext {
fatalError()
}
let controller = GiftStoreScreen(context: context, starsContext: starsContext, peerId: peerId, gift: gift)
controller.navigationPresentation = .modal
return controller
}
@ -3668,8 +3667,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return StarsWithdrawScreen(context: context, mode: .accountWithdraw, completion: completion)
}
public func makeStarGiftResellScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController {
return StarsWithdrawScreen(context: context, mode: .starGiftResell, completion: completion)
public func makeStarGiftResellScreen(context: AccountContext, update: Bool, completion: @escaping (Int64) -> Void) -> ViewController {
return StarsWithdrawScreen(context: context, mode: .starGiftResell(update), completion: completion)
}
public func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController {
@ -3689,7 +3688,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}
public func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: ((StarGift.UniqueGift) -> Void)?, dismissed: (() -> Void)?) -> ViewController {
let controller = GiftViewScreen(context: context, subject: .uniqueGift(gift), shareStory: shareStory)
let controller = GiftViewScreen(context: context, subject: .uniqueGift(gift, nil), shareStory: shareStory)
controller.disposed = {
dismissed?()
}