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 {

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
@ -2572,6 +2586,12 @@ public final class ResaleGiftsContext {
}
}
public func removeStarGift(gift: TelegramCore.StarGift) {
self.impl.with { impl in
impl.removeStarGift(gift: gift)
}
}
public var currentState: ResaleGiftsContext.State? {
var state: ResaleGiftsContext.State?
self.impl.syncWith { impl in

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 {
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))
)
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>("")
let attributes = self.state?.starGiftsState?.attributes ?? []
let modelAttributes = attributes.filter { attribute in
if case .model = attribute {
return true
} else {
return false
}
}
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] = []
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)
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
}
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 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)
let contextController = ContextController(
context: component.context,
presentationData: presentationData,
source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)),
items: .single(ContextController.Items(content: .list(items))),
gesture: nil
)
]
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)
controller.presentInGlobalOverlay(contextController)
}
@ -539,28 +424,70 @@ final class GiftStoreScreenComponent: Component {
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let searchQueryPromise = ValuePromise<String>("")
let attributes = self.state?.starGiftsState?.attributes ?? []
let backdropAttributes = attributes.filter { attribute in
if case .backdrop = attribute {
return true
} else {
return false
}
}
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] = []
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)
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
}
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 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)
let contextController = ContextController(
context: component.context,
presentationData: presentationData,
source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)),
items: .single(ContextController.Items(content: .list(items))),
gesture: nil
)
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)
controller.presentInGlobalOverlay(contextController)
}
@ -630,28 +514,70 @@ final class GiftStoreScreenComponent: Component {
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let searchQueryPromise = ValuePromise<String>("")
let attributes = self.state?.starGiftsState?.attributes ?? []
let patternAttributes = attributes.filter { attribute in
if case .pattern = attribute {
return true
} else {
return false
}
}
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] = []
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)
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
}
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 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)
let contextController = ContextController(
context: component.context,
presentationData: presentationData,
source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)),
items: .single(ContextController.Items(content: .list(items))),
gesture: nil
)
]
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)
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,13 +690,12 @@ 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(
@ -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

@ -53,11 +53,13 @@ private final class GiftViewSheetContent: CombinedComponent {
let convertToStars: () -> Void
let openStarsIntro: () -> Void
let sendGift: (EnginePeer.Id) -> Void
let changeRecipient: () -> Void
let openMyGifts: () -> Void
let transferGift: () -> Void
let upgradeGift: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)
let buyGift: ((String, EnginePeer.Id) -> Signal<Never, BuyStarGiftError>)
let shareGift: () -> Void
let resellGift: () -> Void
let resellGift: (Bool) -> Void
let showAttributeInfo: (Any, String) -> Void
let viewUpgraded: (EngineMessage.Id) -> Void
let openMore: (ASDisplayNode, ContextGesture?) -> Void
@ -74,11 +76,13 @@ private final class GiftViewSheetContent: CombinedComponent {
convertToStars: @escaping () -> Void,
openStarsIntro: @escaping () -> Void,
sendGift: @escaping (EnginePeer.Id) -> Void,
changeRecipient: @escaping () -> Void,
openMyGifts: @escaping () -> Void,
transferGift: @escaping () -> Void,
upgradeGift: @escaping ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>),
buyGift: @escaping ((String, EnginePeer.Id) -> Signal<Never, BuyStarGiftError>),
shareGift: @escaping () -> Void,
resellGift: @escaping () -> Void,
resellGift: @escaping (Bool) -> Void,
showAttributeInfo: @escaping (Any, String) -> Void,
viewUpgraded: @escaping (EngineMessage.Id) -> Void,
openMore: @escaping (ASDisplayNode, ContextGesture?) -> Void,
@ -94,9 +98,11 @@ private final class GiftViewSheetContent: CombinedComponent {
self.convertToStars = convertToStars
self.openStarsIntro = openStarsIntro
self.sendGift = sendGift
self.changeRecipient = changeRecipient
self.openMyGifts = openMyGifts
self.transferGift = transferGift
self.upgradeGift = upgradeGift
self.buyGift = buyGift
self.shareGift = shareGift
self.resellGift = resellGift
self.showAttributeInfo = showAttributeInfo
@ -118,12 +124,22 @@ private final class GiftViewSheetContent: CombinedComponent {
final class State: ComponentState {
private let context: AccountContext
private(set) var subject: GiftViewScreen.Subject
private let upgradeGift: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)
private let buyGift: ((String, EnginePeer.Id) -> Signal<Never, BuyStarGiftError>)
private let getController: () -> ViewController?
private var disposable: Disposable?
var initialized = false
var recipientPeerIdPromise = ValuePromise<EnginePeer.Id?>(nil)
var recipientPeerId: EnginePeer.Id? {
didSet {
self.recipientPeerIdPromise.set(self.recipientPeerId)
}
}
var peerMap: [EnginePeer.Id: EnginePeer] = [:]
var starGiftsMap: [Int64: StarGift.Gift] = [:]
@ -161,11 +177,13 @@ private final class GiftViewSheetContent: CombinedComponent {
context: AccountContext,
subject: GiftViewScreen.Subject,
upgradeGift: @escaping ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>),
buyGift: @escaping ((String, EnginePeer.Id) -> Signal<Never, BuyStarGiftError>),
getController: @escaping () -> ViewController?
) {
self.context = context
self.subject = subject
self.upgradeGift = upgradeGift
self.buyGift = buyGift
self.getController = getController
super.init()
@ -187,6 +205,7 @@ private final class GiftViewSheetContent: CombinedComponent {
peerIds.append(contentsOf: media.peerIds)
}
}
if case let .unique(gift) = arguments.gift {
if case let .peerId(peerId) = gift.owner {
peerIds.append(peerId)
@ -247,18 +266,38 @@ private final class GiftViewSheetContent: CombinedComponent {
}
}
let peerIdsSignal: Signal<[EnginePeer.Id], NoError>
if case let .uniqueGift(_, recipientPeerIdValue) = subject, let recipientPeerIdValue {
self.recipientPeerId = recipientPeerIdValue
self.recipientPeerIdPromise.set(recipientPeerIdValue)
peerIdsSignal = self.recipientPeerIdPromise.get()
|> map { recipientPeerId in
var peerIds = peerIds
if let recipientPeerId {
peerIds.append(recipientPeerId)
}
return peerIds
}
} else {
peerIdsSignal = .single(peerIds)
}
self.disposable = combineLatest(queue: Queue.mainQueue(),
context.engine.data.get(EngineDataMap(
peerIdsSignal
|> distinctUntilChanged
|> mapToSignal { peerIds in
return context.engine.data.get(EngineDataMap(
peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in
return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
}
)),
))
},
.single(nil) |> then(context.engine.payments.cachedStarGifts())
).startStrict(next: { [weak self] peers, starGifts in
if let strongSelf = self {
var peersMap: [EnginePeer.Id: EnginePeer] = [:]
for peerId in peerIds {
if let maybePeer = peers[peerId], let peer = maybePeer {
for (peerId, maybePeer) in peers {
if let peer = maybePeer {
peersMap[peerId] = peer
}
}
@ -281,7 +320,12 @@ private final class GiftViewSheetContent: CombinedComponent {
})
}
if let starsContext = context.starsContext, let state = starsContext.currentState, state.balance < StarsAmount(value: 100, nanos: 0) {
var minRequiredAmount = StarsAmount(value: 100, nanos: 0)
if let resellStars = self.subject.arguments?.resellStars {
minRequiredAmount = StarsAmount(value: resellStars, nanos: 0)
}
if let starsContext = context.starsContext, let state = starsContext.currentState, state.balance < minRequiredAmount {
self.optionsPromise.set(context.engine.payments.starsTopUpOptions()
|> map(Optional.init))
}
@ -289,7 +333,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if let controller = getController() as? GiftViewScreen {
controller.updateSubject.connect { [weak self] subject in
self?.subject = subject
self?.updated()
self?.updated(transition: .easeInOut(duration: 0.25))
}
}
}
@ -359,35 +403,124 @@ private final class GiftViewSheetContent: CombinedComponent {
}
}
func changeRecipient() {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let mode = ContactSelectionControllerMode.starsGifting(birthdays: nil, hasActions: false, showSelf: true, selfSubtitle: presentationData.strings.Premium_Gift_ContactSelection_BuySelf)
//TODO:localize
let controller = self.context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(
context: context,
mode: mode,
autoDismiss: true,
title: { _ in return "Change Recipient" },
options: .single([]),
allowChannelsInSearch: false
))
controller.navigationPresentation = .modal
let _ = (controller.result
|> deliverOnMainQueue).start(next: { [weak self] result in
if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer {
self?.recipientPeerId = peer.id
}
})
self.getController()?.push(controller)
}
func commitBuy() {
guard let arguments = self.subject.arguments, let _ = arguments.peerId, let starsContext = self.context.starsContext, let starsState = starsContext.currentState, case let .unique(uniqueGift) = arguments.gift else {
guard let resellStars = self.subject.arguments?.resellStars, let starsContext = self.context.starsContext, let starsState = starsContext.currentState, case let .unique(uniqueGift) = self.subject.arguments?.gift else {
return
}
let giftTitle = "\(uniqueGift.title) #\(uniqueGift.number)"
let context = self.context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let recipientPeerId = self.recipientPeerId ?? self.context.account.peerId
let action = {
let proceed: (Int64) -> Void = { formId in
self.inProgress = true
self.updated()
let signal = self.context.engine.payments.sendStarsPaymentForm(formId: formId, source: .starGiftResale(slug: uniqueGift.slug, toPeerId: self.context.account.peerId))
|> mapError { _ -> SendBotPaymentFormError in
return .generic
}
|> mapToSignal { result in
if case let .done(_, _, gift) = result, let gift {
return .single(gift)
} else {
return .complete()
}
}
self.buyDisposable = (signal
|> deliverOnMainQueue).start(next: { [weak self, weak starsContext] result in
self.buyDisposable = (self.buyGift(uniqueGift.slug, recipientPeerId)
|> deliverOnMainQueue).start(completed: { [weak self, weak starsContext] in
guard let self, let controller = self.getController() as? GiftViewScreen else {
return
}
self.inProgress = false
var animationFile: TelegramMediaFile?
for attribute in uniqueGift.attributes {
if case let .model(_, file, _) = attribute {
animationFile = file
break
}
}
if let navigationController = controller.navigationController as? NavigationController {
if recipientPeerId == self.context.account.peerId {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in
guard let peer, let navigationController else {
return
}
var controllers = Array(navigationController.viewControllers.prefix(1))
if let controller = context.sharedContext.makePeerInfoController(
context: context,
updatedPresentationData: nil,
peer: peer._asPeer(),
mode: .myProfileGifts,
avatarInitiallyExpanded: false,
fromChat: false,
requestsContext: nil
) {
controllers.append(controller)
}
navigationController.setViewControllers(controllers, animated: true)
navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds))
Queue.mainQueue().after(0.5, {
if let lastController = navigationController.viewControllers.last as? ViewController, let animationFile {
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .sticker(context: context, file: animationFile, loop: false, title: "Gift Acquired", text: "\(giftTitle) is now yours.", undoText: nil, customAction: nil),
elevatedLayout: lastController is ChatController,
action: { _ in
return true
}
)
lastController.present(resultController, in: .window(.root))
}
})
})
} else {
var controllers = Array(navigationController.viewControllers.prefix(1))
let chatController = self.context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: recipientPeerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
chatController.hintPlayNextOutgoingGift()
controllers.append(chatController)
navigationController.setViewControllers(controllers, animated: true)
Queue.mainQueue().after(0.5, {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: recipientPeerId))
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in
if let peer, let lastController = navigationController?.viewControllers.last as? ViewController, let animationFile {
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .sticker(context: context, file: animationFile, loop: false, title: "Gift Sent", text: "\(peer.compactDisplayTitle) has been notified about your gift.", undoText: nil, customAction: nil),
elevatedLayout: lastController is ChatController,
action: { _ in
return true
}
)
lastController.present(resultController, in: .window(.root))
}
})
})
}
}
controller.animateSuccess()
self.updated(transition: .spring(duration: 0.4))
@ -432,13 +565,25 @@ private final class GiftViewSheetContent: CombinedComponent {
}
}
}
let giftTitle = "\(uniqueGift.title) #\(uniqueGift.number)"
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: recipientPeerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
}
let text: String
if recipientPeerId == self.context.account.peerId {
text = "Do you really want to buy **\(giftTitle)** for **\(resellStars)** Stars?"
} else {
text = "Do you really want to buy **\(giftTitle)** for **\(resellStars)** Stars and gift it to **\(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))**?"
}
let alertController = textAlertController(
context: self.context,
title: "Confirm Payment",
text: "Do you really want to buy **\(giftTitle)** for **\(arguments.resellStars ?? 0)** Stars?",
text: text,
actions: [
TextAlertAction(type: .defaultAction, title: "Buy for \(arguments.resellStars ?? 0) Stars", action: {
TextAlertAction(type: .defaultAction, title: "Buy for \(resellStars) Stars", action: {
action()
}),
TextAlertAction(type: .genericAction, title: "Cancel", action: {
@ -450,6 +595,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if let controller = self.getController() as? GiftViewScreen {
controller.present(alertController, in: .window(.root))
}
})
}
func commitUpgrade() {
@ -527,10 +673,12 @@ private final class GiftViewSheetContent: CombinedComponent {
}
func makeState() -> State {
return State(context: self.context, subject: self.subject, upgradeGift: self.upgradeGift, getController: self.getController)
return State(context: self.context, subject: self.subject, upgradeGift: self.upgradeGift, buyGift: self.buyGift, getController: self.getController)
}
static var body: Body {
let priceButton = Child(PlainButtonComponent.self)
let buttons = Child(ButtonsComponent.self)
let animation = Child(GiftCompositionComponent.self)
let title = Child(MultilineTextComponent.self)
@ -538,7 +686,6 @@ private final class GiftViewSheetContent: CombinedComponent {
let transferButton = Child(PlainButtonComponent.self)
let wearButton = Child(PlainButtonComponent.self)
// let shareButton = Child(PlainButtonComponent.self)
let resellButton = Child(PlainButtonComponent.self)
let wearAvatar = Child(AvatarComponent.self)
@ -599,6 +746,7 @@ private final class GiftViewSheetContent: CombinedComponent {
var uniqueGift: StarGift.UniqueGift?
var isSelfGift = false
var isChannelGift = false
var isMyUniqueGift = false
if case let .soldOutGift(gift) = subject {
animationFile = gift.file
@ -646,6 +794,10 @@ private final class GiftViewSheetContent: CombinedComponent {
isSelfGift = arguments.messageId?.peerId == component.context.account.peerId
if case let .peerId(peerId) = uniqueGift?.owner, peerId == component.context.account.peerId || isChannelGift {
isMyUniqueGift = true
}
if isSelfGift {
titleString = strings.Gift_View_Self_Title
} else {
@ -1148,12 +1300,7 @@ private final class GiftViewSheetContent: CombinedComponent {
var descriptionText: String
if let uniqueGift {
titleString = uniqueGift.title
if let resellPrice = uniqueGift.resellStars, incoming {
descriptionText = "\(strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator)) • Listed for * \(resellPrice)"
} else {
descriptionText = "\(strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))"
}
} else if soldOut {
descriptionText = strings.Gift_View_UnavailableDescription
} else if upgraded {
@ -1341,6 +1488,43 @@ private final class GiftViewSheetContent: CombinedComponent {
if !soldOut {
if let uniqueGift {
if case let .uniqueGift(_, recipientPeerIdValue) = component.subject, let _ = recipientPeerIdValue, let recipientPeerId = state.recipientPeerId {
//TODO:localize
if let peer = state.peerMap[recipientPeerId] {
tableItems.append(.init(
id: "recipient",
title: "Recipient",
component: AnyComponent(
Button(
content: AnyComponent(
HStack([
AnyComponentWithIdentity(
id: AnyHashable(peer.id),
component: AnyComponent(PeerCellComponent(
context: component.context,
theme: theme,
strings: strings,
peer: peer
))
),
AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(ButtonContentComponent(
context: component.context,
text: "change",
color: theme.list.itemAccentColor
))
)
], spacing: 4.0)
),
action: { [weak state] in
state?.changeRecipient()
}
)
)
))
}
} else {
switch uniqueGift.owner {
case let .peerId(peerId):
if let peer = state.peerMap[peerId] {
@ -1478,6 +1662,7 @@ private final class GiftViewSheetContent: CombinedComponent {
)
))
}
}
} else if let peerId = subject.arguments?.fromPeerId, let peer = state.peerMap[peerId] {
var isBot = false
if case let .user(user) = peer, user.botInfo != nil {
@ -1568,7 +1753,7 @@ private final class GiftViewSheetContent: CombinedComponent {
}
if let uniqueGift {
if case let .peerId(peerId) = uniqueGift.owner, peerId == component.context.account.peerId || isChannelGift {
if isMyUniqueGift, case let .peerId(peerId) = uniqueGift.owner {
var canTransfer = true
if let peer = state.peerMap[peerId], case let .channel(channel) = peer, !channel.flags.contains(.isCreator) {
canTransfer = false
@ -1679,7 +1864,7 @@ private final class GiftViewSheetContent: CombinedComponent {
),
effectAlignment: .center,
action: {
component.resellGift()
component.resellGift(false)
}
),
environment: {},
@ -1691,29 +1876,6 @@ private final class GiftViewSheetContent: CombinedComponent {
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
// let shareButton = shareButton.update(
// component: PlainButtonComponent(
// content: AnyComponent(
// HeaderButtonComponent(
// title: strings.Gift_View_Header_Share,
// iconName: "Premium/Collectible/Share"
// )
// ),
// effectAlignment: .center,
// action: {
// component.shareGift()
// }
// ),
// environment: {},
// availableSize: CGSize(width: buttonWidth, height: buttonHeight),
// transition: context.transition
// )
// context.add(shareButton
// .position(CGPoint(x: buttonOriginX + buttonWidth / 2.0, y: headerHeight - buttonHeight / 2.0 - 16.0))
// .appear(.default(scale: true, alpha: true))
// .disappear(.default(scale: true, alpha: true))
// )
}
let showAttributeInfo = component.showAttributeInfo
@ -2066,17 +2228,48 @@ private final class GiftViewSheetContent: CombinedComponent {
}
var resellStars: Int64?
var selling = false
if let uniqueGift {
resellStars = uniqueGift.resellStars
if incoming, let resellStars {
let priceButton = priceButton.update(
component: PlainButtonComponent(
content: AnyComponent(
PriceButtonComponent(price: resellStars)
),
effectAlignment: .center,
action: {
component.resellGift(true)
},
animateScale: false
),
availableSize: CGSize(width: 120.0, height: 30.0),
transition: context.transition
)
context.add(priceButton
.position(CGPoint(x: environment.safeInsets.left + 16.0 + priceButton.size.width / 2.0, y: 28.0))
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
}
if ((incoming && !converted && !upgraded) || exported || (!incoming && resellStars != nil)) && (!showUpgradePreview && !showWearPreview) {
if !incoming, let _ = resellStars {
if case let .uniqueGift(_, recipientPeerId) = component.subject, recipientPeerId != nil {
} else {
selling = true
}
}
}
if ((incoming && !converted && !upgraded) || exported || selling) && (!showUpgradePreview && !showWearPreview) {
let linkColor = theme.actionSheet.controlAccentColor
if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme {
state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme)
}
var addressToOpen: String?
var descriptionText: String
if let uniqueGift, !incoming {
if let uniqueGift, selling {
//TODO:localize
let ownerName: String
if case let .peerId(peerId) = uniqueGift.owner {
@ -2377,10 +2570,11 @@ private final class GiftViewSheetContent: CombinedComponent {
availableSize: buttonSize,
transition: context.transition
)
} else if !incoming, let resellStars {
} else if !incoming, let resellStars, !isMyUniqueGift {
if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme {
state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme)
}
//TODO:localize
var upgradeString = "Buy for"
upgradeString += " # \(presentationStringsFormattedNumber(Int32(resellStars), environment.dateTimeFormat.groupingSeparator))"
@ -2454,11 +2648,13 @@ private final class GiftViewSheetComponent: CombinedComponent {
let convertToStars: () -> Void
let openStarsIntro: () -> Void
let sendGift: (EnginePeer.Id) -> Void
let changeRecipient: () -> Void
let openMyGifts: () -> Void
let transferGift: () -> Void
let upgradeGift: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)
let buyGift: ((String, EnginePeer.Id) -> Signal<Never, BuyStarGiftError>)
let shareGift: () -> Void
let resellGift: () -> Void
let resellGift: (Bool) -> Void
let viewUpgraded: (EngineMessage.Id) -> Void
let openMore: (ASDisplayNode, ContextGesture?) -> Void
let showAttributeInfo: (Any, String) -> Void
@ -2473,11 +2669,13 @@ private final class GiftViewSheetComponent: CombinedComponent {
convertToStars: @escaping () -> Void,
openStarsIntro: @escaping () -> Void,
sendGift: @escaping (EnginePeer.Id) -> Void,
changeRecipient: @escaping () -> Void,
openMyGifts: @escaping () -> Void,
transferGift: @escaping () -> Void,
upgradeGift: @escaping ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>),
buyGift: @escaping ((String, EnginePeer.Id) -> Signal<Never, BuyStarGiftError>),
shareGift: @escaping () -> Void,
resellGift: @escaping () -> Void,
resellGift: @escaping (Bool) -> Void,
viewUpgraded: @escaping (EngineMessage.Id) -> Void,
openMore: @escaping (ASDisplayNode, ContextGesture?) -> Void,
showAttributeInfo: @escaping (Any, String) -> Void
@ -2491,9 +2689,11 @@ private final class GiftViewSheetComponent: CombinedComponent {
self.convertToStars = convertToStars
self.openStarsIntro = openStarsIntro
self.sendGift = sendGift
self.changeRecipient = changeRecipient
self.openMyGifts = openMyGifts
self.transferGift = transferGift
self.upgradeGift = upgradeGift
self.buyGift = buyGift
self.shareGift = shareGift
self.resellGift = resellGift
self.viewUpgraded = viewUpgraded
@ -2542,9 +2742,11 @@ private final class GiftViewSheetComponent: CombinedComponent {
convertToStars: context.component.convertToStars,
openStarsIntro: context.component.openStarsIntro,
sendGift: context.component.sendGift,
changeRecipient: context.component.changeRecipient,
openMyGifts: context.component.openMyGifts,
transferGift: context.component.transferGift,
upgradeGift: context.component.upgradeGift,
buyGift: context.component.buyGift,
shareGift: context.component.shareGift,
resellGift: context.component.resellGift,
showAttributeInfo: context.component.showAttributeInfo,
@ -2574,6 +2776,7 @@ private final class GiftViewSheetComponent: CombinedComponent {
if animated {
if let controller = controller() as? GiftViewScreen {
controller.dismissAllTooltips()
controller.dismissBalanceOverlay()
animateOut.invoke(Action { _ in
controller.dismiss(completion: nil)
})
@ -2581,6 +2784,7 @@ private final class GiftViewSheetComponent: CombinedComponent {
} else {
if let controller = controller() as? GiftViewScreen {
controller.dismissAllTooltips()
controller.dismissBalanceOverlay()
controller.dismiss(completion: nil)
}
}
@ -2619,7 +2823,7 @@ private final class GiftViewSheetComponent: CombinedComponent {
public class GiftViewScreen: ViewControllerComponentContainer {
public enum Subject: Equatable {
case message(EngineMessage)
case uniqueGift(StarGift.UniqueGift)
case uniqueGift(StarGift.UniqueGift, EnginePeer.Id?)
case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift)
case soldOutGift(StarGift.Gift)
case upgradePreview([StarGift.UniqueGift.Attribute], String)
@ -2667,8 +2871,8 @@ public class GiftViewScreen: ViewControllerComponentContainer {
return nil
}
}
case let .uniqueGift(gift), let .wearPreview(gift):
return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, nil, nil, nil)
case let .uniqueGift(gift, _), let .wearPreview(gift):
return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellStars, nil, nil)
case let .profileGift(peerId, gift):
var messageId: EngineMessage.Id?
if case let .message(messageIdValue) = gift.reference {
@ -2715,6 +2919,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
convertToStars: (() -> Void)? = nil,
transferGift: ((Bool, EnginePeer.Id) -> Signal<Never, TransferStarGiftError>)? = nil,
upgradeGift: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)? = nil,
buyGift: ((String, EnginePeer.Id) -> Signal<Never, BuyStarGiftError>)? = nil,
updateResellStars: ((Int64?) -> Void)? = nil,
togglePinnedToTop: ((Bool) -> Bool)? = nil,
shareStory: ((StarGift.UniqueGift) -> Void)? = nil
@ -2732,8 +2937,9 @@ public class GiftViewScreen: ViewControllerComponentContainer {
var openMyGiftsImpl: (() -> Void)?
var transferGiftImpl: (() -> Void)?
var upgradeGiftImpl: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)?
var buyGiftImpl: ((String, EnginePeer.Id) -> Signal<Never, BuyStarGiftError>)?
var shareGiftImpl: (() -> Void)?
var resellGiftImpl: (() -> Void)?
var resellGiftImpl: ((Bool) -> Void)?
var openMoreImpl: ((ASDisplayNode, ContextGesture?) -> Void)?
var showAttributeInfoImpl: ((Any, String) -> Void)?
var viewUpgradedImpl: ((EngineMessage.Id) -> Void)?
@ -2763,6 +2969,9 @@ public class GiftViewScreen: ViewControllerComponentContainer {
},
sendGift: { peerId in
sendGiftImpl?(peerId)
},
changeRecipient: {
},
openMyGifts: {
openMyGiftsImpl?()
@ -2773,11 +2982,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
upgradeGift: { formId, keepOriginalInfo in
return upgradeGiftImpl?(formId, keepOriginalInfo) ?? .complete()
},
buyGift: { slug, peerId in
return buyGiftImpl?(slug, peerId) ?? .complete()
},
shareGift: {
shareGiftImpl?()
},
resellGift: {
resellGiftImpl?()
resellGift: { update in
resellGiftImpl?(update)
},
viewUpgraded: { messageId in
viewUpgradedImpl?(messageId)
@ -3077,6 +3289,11 @@ public class GiftViewScreen: ViewControllerComponentContainer {
}
if let upgradeGift {
return upgradeGift(formId, keepOriginalInfo)
|> afterCompleted {
if formId != nil {
context.starsContext?.load(force: true)
}
}
} else {
return self.context.engine.payments.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo)
|> afterCompleted {
@ -3087,6 +3304,23 @@ public class GiftViewScreen: ViewControllerComponentContainer {
}
}
buyGiftImpl = { [weak self] slug, peerId in
guard let self else {
return .complete()
}
if let buyGift {
return buyGift(slug, peerId)
|> afterCompleted {
context.starsContext?.load(force: true)
}
} else {
return self.context.engine.payments.buyStarGift(slug: slug, peerId: peerId)
|> afterCompleted {
context.starsContext?.load(force: true)
}
}
}
shareGiftImpl = { [weak self] in
guard let self, let arguments = self.subject.arguments, case let .unique(gift) = arguments.gift else {
return
@ -3162,13 +3396,15 @@ public class GiftViewScreen: ViewControllerComponentContainer {
self.present(shareController, in: .window(.root))
}
resellGiftImpl = { [weak self] in
resellGiftImpl = { [weak self] update in
guard let self, let arguments = self.subject.arguments, case let .profileGift(peerId, currentSubject) = self.subject, case let .unique(gift) = arguments.gift else {
return
}
self.dismissAllTooltips()
//TODO:localize
if let resellStars = gift.resellStars, resellStars > 0 {
if let resellStars = gift.resellStars, resellStars > 0, !update {
let alertController = textAlertController(
context: context,
title: "Unlist This Item?",
@ -3217,7 +3453,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
)
self.present(alertController, in: .window(.root))
} else {
let resellController = context.sharedContext.makeStarGiftResellScreen(context: context, completion: { [weak self] price in
let resellController = context.sharedContext.makeStarGiftResellScreen(context: context, update: update, completion: { [weak self] price in
guard let self else {
return
}
@ -3226,7 +3462,11 @@ public class GiftViewScreen: ViewControllerComponentContainer {
let giftTitle = "\(gift.title) #\(gift.number)"
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let text = "\(giftTitle) is now for sale!"
var text = "\(giftTitle) is now for sale!"
if update {
text = "\(giftTitle) is relisted for \(price) Stars."
}
let tooltipController = UndoOverlayController(
presentationData: presentationData,
@ -3421,6 +3661,10 @@ public class GiftViewScreen: ViewControllerComponentContainer {
view.dismissAnimated()
}
self.dismissBalanceOverlay()
}
fileprivate func dismissBalanceOverlay() {
if let view = self.balanceOverlay.view, view.superview != nil {
view.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4, removeOnCompletion: false)
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)

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?()
}