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 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, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsWithdrawalScreen(context: AccountContext, 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 makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController
func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> ViewController func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> ViewController
func makeStarsIntroScreen(context: AccountContext) -> ViewController func makeStarsIntroScreen(context: AccountContext) -> ViewController

View File

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

View File

@ -476,6 +476,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
private var size: CGSize? private var size: CGSize?
private let backgroundAnimationNode: ASImageNode private let backgroundAnimationNode: ASImageNode
private var iconNode: ASImageNode?
fileprivate let textNode: ImmediateTextNode fileprivate let textNode: ImmediateTextNode
private var badgeNode: BadgeNode? private var badgeNode: BadgeNode?
private let statusNode: SemanticStatusNode private let statusNode: SemanticStatusNode
@ -781,6 +782,25 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
badgeNode.removeFromSupernode() 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 { if self.textNode.frame.width.isZero {
self.textNode.frame = textFrame self.textNode.frame = textFrame
} else { } else {
@ -795,7 +815,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
self.transitionFromProgress() self.transitionFromProgress()
} }
} }
if let shimmerView = self.shimmerView, let borderView = self.borderView, let borderMaskView = self.borderMaskView, let borderShimmerView = self.borderShimmerView { if let shimmerView = self.shimmerView, let borderView = self.borderView, let borderMaskView = self.borderMaskView, let borderShimmerView = self.borderShimmerView {
let buttonFrame = CGRect(origin: .zero, size: size) let buttonFrame = CGRect(origin: .zero, size: size)
let buttonWidth = size.width let buttonWidth = size.width

View File

@ -2145,6 +2145,8 @@ public protocol ContextReferenceContentSource: AnyObject {
var shouldBeDismissed: Signal<Bool, NoError> { get } var shouldBeDismissed: Signal<Bool, NoError> { get }
var forceDisplayBelowKeyboard: Bool { get }
func transitionInfo() -> ContextControllerReferenceViewInfo? func transitionInfo() -> ContextControllerReferenceViewInfo?
} }
@ -2153,6 +2155,10 @@ public extension ContextReferenceContentSource {
return false return false
} }
var forceDisplayBelowKeyboard: Bool {
return false
}
var shouldBeDismissed: Signal<Bool, NoError> { var shouldBeDismissed: Signal<Bool, NoError> {
return .single(false) return .single(false)
} }
@ -2744,7 +2750,9 @@ public final class ContextController: ViewController, StandalonePresentableContr
} }
public func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?) { 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.dismissOnInputClose = (result, completion)
self.view.endEditing(true) self.view.endEditing(true)
return return

View File

@ -273,7 +273,7 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
return super.hitTest(point, with: event) return super.hitTest(point, with: event)
} }
func setItem(item: ContextMenuActionItem) { public func setItem(item: ContextMenuActionItem) {
self.item = item self.item = item
self.accessibilityLabel = item.text 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) return ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: fileId, enableAnimation: true), range: entity.range)
} else if case .Bold = entity.type { } else if case .Bold = entity.type {
return ChatTextInputStateTextAttribute(type: .bold, range: entity.range) return ChatTextInputStateTextAttribute(type: .bold, range: entity.range)
} else if case .Italic = entity.type {
return ChatTextInputStateTextAttribute(type: .italic, range: entity.range)
} }
return nil return nil
}) })
@ -373,6 +375,8 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
], range: NSRange(location: 0, length: result.length)) ], range: NSRange(location: 0, length: result.length))
for attribute in inputStateText.attributes { for attribute in inputStateText.attributes {
if case .bold = attribute.type { 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)) 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 { func wantsDisplayBelowKeyboard() -> Bool {
if let reactionContextNode = self.reactionContextNode { if let reactionContextNode = self.reactionContextNode {
return reactionContextNode.wantsDisplayBelowKeyboard() return reactionContextNode.wantsDisplayBelowKeyboard()
} else if case let .reference(source) = self.source {
return source.forceDisplayBelowKeyboard
} else { } else {
return false return false
} }

View File

@ -1823,6 +1823,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
private var isDismissing = false private var isDismissing = false
fileprivate let mainButtonStatePromise = Promise<AttachmentMainButtonState?>(nil) fileprivate let mainButtonStatePromise = Promise<AttachmentMainButtonState?>(nil)
fileprivate let secondaryButtonStatePromise = Promise<AttachmentMainButtonState?>(nil)
private let mainButtonAction: (() -> Void)? 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.updateAlpha(node: self.moreButtonNode.iconNode, alpha: moreIsVisible ? 1.0 : 0.0)
transition.updateTransformScale(node: self.moreButtonNode.iconNode, scale: moreIsVisible ? 1.0 : 0.1) transition.updateTransformScale(node: self.moreButtonNode.iconNode, scale: moreIsVisible ? 1.0 : 0.1)
//if self. { if self.selectionCount > 0 {
//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))) //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() { private func updateThemeAndStrings() {
@ -2933,6 +2948,10 @@ final class MediaPickerContext: AttachmentMediaPickerContext {
return self.controller?.mainButtonStatePromise.get() ?? .single(nil) return self.controller?.mainButtonStatePromise.get() ?? .single(nil)
} }
public var secondaryButtonState: Signal<AttachmentMainButtonState?, NoError> {
return self.controller?.secondaryButtonStatePromise.get() ?? .single(nil)
}
init(controller: MediaPickerScreenImpl) { init(controller: MediaPickerScreenImpl) {
self.controller = controller self.controller = controller
} }
@ -2952,6 +2971,10 @@ final class MediaPickerContext: AttachmentMediaPickerContext {
func mainButtonAction() { func mainButtonAction() {
self.controller?.mainButtonPressed() self.controller?.mainButtonPressed()
} }
func secondaryButtonAction() {
self.controller?.mainButtonPressed()
}
} }
private final class MediaPickerContextReferenceContentSource: ContextReferenceContentSource { 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) return _internal_transferStarGift(account: self.account, prepaid: prepaid, reference: reference, peerId: peerId)
} }
func buyStarGift(gift inputGift: TelegramCore.StarGift, peerId: EnginePeer.Id) -> Signal<Never, BuyStarGiftError> { func buyStarGift(slug: String, 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()
}
if let count = self.count { if let count = self.count {
self.count = max(0, count - 1) self.count = max(0, count - 1)
} }
self.gifts.removeAll(where: { $0.gift == inputGift }) self.gifts.removeAll(where: { gift in
self.filteredGifts.removeAll(where: { $0.gift == inputGift }) 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() 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) { 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 return Signal { subscriber in
let disposable = MetaDisposable() let disposable = MetaDisposable()
self.impl.with { impl in 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) subscriber.putError(error)
}, completed: { }, completed: {
subscriber.putCompletion() subscriber.putCompletion()
@ -2308,7 +2311,7 @@ private final class ResaleGiftsContextImpl {
private let disposable = MetaDisposable() private let disposable = MetaDisposable()
private var sorting: ResaleGiftsContext.Sorting = .date private var sorting: ResaleGiftsContext.Sorting = .value
private var filterAttributes: [ResaleGiftsContext.Attribute] = [] private var filterAttributes: [ResaleGiftsContext.Attribute] = []
private var gifts: [StarGift] = [] private var gifts: [StarGift] = []
@ -2431,7 +2434,15 @@ private final class ResaleGiftsContextImpl {
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) 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 { if initialNextOffset == nil || reload {
self.gifts = gifts self.gifts = gifts
} else { } else {
for gift in gifts { self.gifts.append(contentsOf: gifts)
self.gifts.append(gift)
}
} }
let updatedCount = max(Int32(self.gifts.count), count) let updatedCount = max(Int32(self.gifts.count), count)
@ -2473,6 +2482,11 @@ private final class ResaleGiftsContextImpl {
self.loadMore() self.loadMore()
} }
func removeStarGift(gift: TelegramCore.StarGift) {
self.gifts.removeAll(where: { $0 == gift })
self.pushState()
}
func updateSorting(_ sorting: ResaleGiftsContext.Sorting) { func updateSorting(_ sorting: ResaleGiftsContext.Sorting) {
guard self.sorting != sorting else { guard self.sorting != sorting else {
return return
@ -2571,6 +2585,12 @@ public final class ResaleGiftsContext {
impl.updateFilterAttributes(attributes) impl.updateFilterAttributes(attributes)
} }
} }
public func removeStarGift(gift: TelegramCore.StarGift) {
self.impl.with { impl in
impl.removeStarGift(gift: gift)
}
}
public var currentState: ResaleGiftsContext.State? { public var currentState: ResaleGiftsContext.State? {
var state: ResaleGiftsContext.State? var state: ResaleGiftsContext.State?

View File

@ -125,7 +125,7 @@ public extension TelegramEngine {
return _internal_transferStarGift(account: self.account, prepaid: prepaid, reference: reference, peerId: peerId) 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) 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.additionalPreviewView.isEnabled = false
self.collageView?.isEnabled = false self.collageView?.isEnabled = false
#if targetEnvironment(simulator)
#else
Queue.mainQueue().after(0.3) { Queue.mainQueue().after(0.3) {
self.previewBlurPromise.set(true) self.previewBlurPromise.set(true)
} }
#endif
self.camera?.stopCapture() self.camera?.stopCapture()
self.cameraIsActive = false self.cameraIsActive = false
@ -3627,12 +3631,17 @@ public class CameraScreenImpl: ViewController, CameraScreen {
if self.cameraState.isCollageEnabled, let collage = self.node.collage { if self.cameraState.isCollageEnabled, let collage = self.node.collage {
selectionLimit = collage.grid.count - collage.results.count selectionLimit = collage.grid.count - collage.results.count
} else { } else {
selectionLimit = 6 if self.cameraState.isCollageEnabled {
selectionLimit = 6
} else {
selectionLimit = 10
}
} }
//TODO:unmock
controller = self.context.sharedContext.makeStoryMediaPickerScreen( controller = self.context.sharedContext.makeStoryMediaPickerScreen(
context: self.context, context: self.context,
isDark: true, isDark: true,
forCollage: self.cameraState.isCollageEnabled, forCollage: self.cameraState.isCollageEnabled || "".isEmpty,
selectionLimit: selectionLimit, selectionLimit: selectionLimit,
getSourceRect: { [weak self] in getSourceRect: { [weak self] in
if let self { if let self {

View File

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

View File

@ -24,21 +24,24 @@ public final class FilterSelectorComponent: Component {
public struct Item: Equatable { public struct Item: Equatable {
public var id: AnyHashable public var id: AnyHashable
public var iconName: String?
public var title: String public var title: String
public var action: (UIView) -> Void public var action: (UIView) -> Void
public init( public init(
id: AnyHashable, id: AnyHashable,
iconName: String? = nil,
title: String, title: String,
action: @escaping (UIView) -> Void action: @escaping (UIView) -> Void
) { ) {
self.id = id self.id = id
self.iconName = iconName
self.title = title self.title = title
self.action = action self.action = action
} }
public static func ==(lhs: Item, rhs: Item) -> Bool { 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( component: AnyComponent(PlainButtonComponent(
content: AnyComponent(ItemComponent( content: AnyComponent(ItemComponent(
context: component.context, context: component.context,
iconName: item.iconName,
text: item.title, text: item.title,
font: itemFont, font: itemFont,
color: component.colors.foreground, color: component.colors.foreground,
@ -231,6 +235,7 @@ extension CGRect {
private final class ItemComponent: CombinedComponent { private final class ItemComponent: CombinedComponent {
let context: AccountContext? let context: AccountContext?
let iconName: String?
let text: String let text: String
let font: UIFont let font: UIFont
let color: UIColor let color: UIColor
@ -238,12 +243,14 @@ private final class ItemComponent: CombinedComponent {
init( init(
context: AccountContext?, context: AccountContext?,
iconName: String?,
text: String, text: String,
font: UIFont, font: UIFont,
color: UIColor, color: UIColor,
backgroundColor: UIColor backgroundColor: UIColor
) { ) {
self.context = context self.context = context
self.iconName = iconName
self.text = text self.text = text
self.font = font self.font = font
self.color = color self.color = color
@ -254,6 +261,9 @@ private final class ItemComponent: CombinedComponent {
if lhs.context !== rhs.context { if lhs.context !== rhs.context {
return false return false
} }
if lhs.iconName != rhs.iconName {
return false
}
if lhs.text != rhs.text { if lhs.text != rhs.text {
return false return false
} }
@ -297,17 +307,22 @@ private final class ItemComponent: CombinedComponent {
let icon = icon.update( let icon = icon.update(
component: BundleIconComponent( component: BundleIconComponent(
name: "Item List/ExpandableSelectorArrows", name: component.iconName ?? "Item List/ExpandableSelectorArrows",
tintColor: component.color tintColor: component.color,
maxSize: component.iconName != nil ? CGSize(width: 22.0, height: 22.0) : nil
), ),
availableSize: CGSize(width: 100, height: 100), availableSize: CGSize(width: 100, height: 100),
transition: .immediate transition: .immediate
) )
let padding: CGFloat = 12.0 let padding: CGFloat = 12.0
var leftPadding = padding
if let _ = component.iconName {
leftPadding -= 4.0
}
let spacing: CGFloat = 4.0 let spacing: CGFloat = 4.0
let totalWidth = title.size.width + icon.size.width + spacing 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( let background = background.update(
component: RoundedRectangle( component: RoundedRectangle(
color: component.backgroundColor, color: component.backgroundColor,
@ -319,12 +334,21 @@ private final class ItemComponent: CombinedComponent {
context.add(background context.add(background
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0)) .position(CGPoint(x: size.width / 2.0, y: size.height / 2.0))
) )
context.add(title if let _ = component.iconName {
.position(CGPoint(x: padding + title.size.width / 2.0, y: size.height / 2.0)) 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: size.width - padding - icon.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 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 var starsItems: [AnyHashable: ComponentView<Empty>] = [:]
private let filterSelector = ComponentView<Empty>() private let filterSelector = ComponentView<Empty>()
private var isLoading = false
private var isUpdating: Bool = false private var isUpdating: Bool = false
@ -144,48 +143,13 @@ final class GiftStoreScreenComponent: Component {
private var effectiveGifts: [StarGift]? { private var effectiveGifts: [StarGift]? {
if let gifts = self.state?.starGiftsState?.gifts { if let gifts = self.state?.starGiftsState?.gifts {
return 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 { } else {
return nil return nil
} }
} }
private func updateScrolling(interactive: Bool = false, transition: ComponentTransition) { 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 return
} }
@ -267,7 +231,7 @@ final class GiftStoreScreenComponent: Component {
), ),
effectAlignment: .center, effectAlignment: .center,
action: { [weak self] in 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 { if let controller = controller() as? GiftStoreScreen {
let mainController: ViewController let mainController: ViewController
if let parentController = controller.parentController() { if let parentController = controller.parentController() {
@ -277,7 +241,7 @@ final class GiftStoreScreenComponent: Component {
} }
let giftController = GiftViewScreen( let giftController = GiftViewScreen(
context: component.context, context: component.context,
subject: .uniqueGift(uniqueGift) subject: .uniqueGift(uniqueGift, state.peerId)
) )
mainController.push(giftController) 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) { func openSortContextMenu(sourceView: UIView) {
guard let component = self.component, let controller = self.environment?.controller() else { guard let component = self.component, let controller = self.environment?.controller() else {
return return
@ -441,28 +334,70 @@ final class GiftStoreScreenComponent: Component {
} }
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let searchQueryPromise = ValuePromise<String>("")
var items: [ContextMenuItem] = [] let attributes = self.state?.starGiftsState?.attributes ?? []
var allSelected = true let modelAttributes = attributes.filter { attribute in
if case .model = attribute {
var currentFilterAttributes: [ResaleGiftsContext.Attribute] = [] return true
var selectedIds = Set<Int64>() } else {
return false
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(.action(ContextMenuActionItem(text: "Select All", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? []
}, iconPosition: .left, action: { [weak self] _, f in let selectedModelAttributes = currentFilterAttributes.filter { attribute in
f(.default) if case .model = attribute {
return true
if let self { } else {
return false
}
}
//TODO:localize
var items: [ContextMenuItem] = []
items.append(.custom(SearchContextItem(
context: component.context,
placeholder: "Search",
value: "",
valueChanged: { value in
searchQueryPromise.set(value)
}
), false))
items.append(.separator)
items.append(.custom(GiftAttributeListContextItem(
context: component.context,
attributes: modelAttributes,
selectedAttributes: selectedModelAttributes,
attributeCount: self.state?.starGiftsState?.attributeCount ?? [:],
searchQuery: searchQueryPromise.get(),
attributeSelected: { [weak self] attribute, exclusive in
guard let self else {
return
}
var updatedFilterAttributes: [ResaleGiftsContext.Attribute]
if exclusive {
updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .model = attribute {
return false
}
return true
}
updatedFilterAttributes.append(attribute)
} else {
updatedFilterAttributes = currentFilterAttributes
if selectedModelAttributes.contains(attribute) {
updatedFilterAttributes.removeAll(where: { $0 == attribute })
} else {
updatedFilterAttributes.append(attribute)
}
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
},
selectAll: { [weak self] in
guard let self else {
return
}
let updatedFilterAttributes = currentFilterAttributes.filter { attribute in let updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .model = attribute { if case .model = attribute {
return false return false
@ -471,65 +406,15 @@ final class GiftStoreScreenComponent: Component {
} }
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
} }
}))) ), false))
if let attributes = self.state?.starGiftsState?.attributes { let contextController = ContextController(
for attribute in attributes { context: component.context,
if case let .model(name, file, _) = attribute { presentationData: presentationData,
let isSelected = allSelected || selectedIds.contains(file.fileId.id) source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)),
items: .single(ContextController.Items(content: .list(items))),
var entities: [MessageTextEntity] = [] gesture: nil
var entityFiles: [Int64: TelegramMediaFile] = [:] )
entities = [
MessageTextEntity(
range: 0..<1,
type: .CustomEmoji(stickerPack: nil, fileId: file.fileId.id)
)
]
entityFiles[file.fileId.id] = file
var title = "# \(name)"
var count = ""
if let counter = self.state?.starGiftsState?.attributeCount[.model(file.fileId.id)] {
count = " \(presentationStringsFormattedNumber(counter, presentationData.dateTimeFormat.groupingSeparator))"
entities.append(
MessageTextEntity(range: title.count ..< title.count + count.count, type: .Bold)
)
title += count
}
items.append(.action(ContextMenuActionItem(text: title, entities: entities, entityFiles: entityFiles, enableEntityAnimations: false, parseMarkdown: true, icon: { theme in
return isSelected ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { [weak self] _, f in
f(.default)
if let self {
var updatedFilterAttributes = currentFilterAttributes
if selectedIds.contains(file.fileId.id) {
updatedFilterAttributes.removeAll(where: { $0 == .model(file.fileId.id) })
} else {
updatedFilterAttributes.append(.model(file.fileId.id))
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
}
}, longPressAction: { [weak self] _, f in
f(.default)
if let self {
var updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .model = attribute {
return false
}
return true
}
updatedFilterAttributes.append(.model(file.fileId.id))
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
}
})))
}
}
}
let contextController = ContextController(context: component.context, presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
controller.presentInGlobalOverlay(contextController) controller.presentInGlobalOverlay(contextController)
} }
@ -539,28 +424,70 @@ final class GiftStoreScreenComponent: Component {
} }
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let searchQueryPromise = ValuePromise<String>("")
var items: [ContextMenuItem] = [] let attributes = self.state?.starGiftsState?.attributes ?? []
var allSelected = true let backdropAttributes = attributes.filter { attribute in
if case .backdrop = attribute {
var currentFilterAttributes: [ResaleGiftsContext.Attribute] = [] return true
var selectedIds = Set<Int32>() } else {
return false
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(.action(ContextMenuActionItem(text: "Select All", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? []
}, iconPosition: .left, action: { [weak self] _, f in let selectedBackdropAttributes = currentFilterAttributes.filter { attribute in
f(.default) if case .backdrop = attribute {
return true
if let self { } else {
return false
}
}
//TODO:localize
var items: [ContextMenuItem] = []
items.append(.custom(SearchContextItem(
context: component.context,
placeholder: "Search",
value: "",
valueChanged: { value in
searchQueryPromise.set(value)
}
), false))
items.append(.separator)
items.append(.custom(GiftAttributeListContextItem(
context: component.context,
attributes: backdropAttributes,
selectedAttributes: selectedBackdropAttributes,
attributeCount: self.state?.starGiftsState?.attributeCount ?? [:],
searchQuery: searchQueryPromise.get(),
attributeSelected: { [weak self] attribute, exclusive in
guard let self else {
return
}
var updatedFilterAttributes: [ResaleGiftsContext.Attribute]
if exclusive {
updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .backdrop = attribute {
return false
}
return true
}
updatedFilterAttributes.append(attribute)
} else {
updatedFilterAttributes = currentFilterAttributes
if selectedBackdropAttributes.contains(attribute) {
updatedFilterAttributes.removeAll(where: { $0 == attribute })
} else {
updatedFilterAttributes.append(attribute)
}
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
},
selectAll: { [weak self] in
guard let self else {
return
}
let updatedFilterAttributes = currentFilterAttributes.filter { attribute in let updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .backdrop = attribute { if case .backdrop = attribute {
return false return false
@ -569,58 +496,15 @@ final class GiftStoreScreenComponent: Component {
} }
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
} }
}))) ), false))
if let attributes = self.state?.starGiftsState?.attributes { let contextController = ContextController(
for attribute in attributes { context: component.context,
if case let .backdrop(name, id, innerColor, outerColor, _, _, _) = attribute { presentationData: presentationData,
let isSelected = allSelected || selectedIds.contains(id) source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)),
items: .single(ContextController.Items(content: .list(items))),
var entities: [MessageTextEntity] = [] gesture: nil
var title = "\(name)" )
var count = ""
if let counter = self.state?.starGiftsState?.attributeCount[.backdrop(id)] {
count = " \(presentationStringsFormattedNumber(counter, presentationData.dateTimeFormat.groupingSeparator))"
entities.append(
MessageTextEntity(range: title.count ..< title.count + count.count, type: .Bold)
)
title += count
}
items.append(.action(ContextMenuActionItem(text: "\(name)\(count)", entities: entities, icon: { theme in
return isSelected ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, additionalLeftIcon: { _ in
return generateGradientFilledCircleImage(diameter: 24.0, colors: [UIColor(rgb: UInt32(bitPattern: innerColor)).cgColor, UIColor(rgb: UInt32(bitPattern: outerColor)).cgColor])
}, action: { [weak self] _, f in
f(.default)
if let self {
var updatedFilterAttributes = currentFilterAttributes
if selectedIds.contains(id) {
updatedFilterAttributes.removeAll(where: { $0 == .backdrop(id) })
} else {
updatedFilterAttributes.append(.backdrop(id))
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
}
}, longPressAction: { [weak self] _, f in
f(.default)
if let self {
var updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .backdrop = attribute {
return false
}
return true
}
updatedFilterAttributes.append(.backdrop(id))
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
}
})))
}
}
}
let contextController = ContextController(context: component.context, presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
controller.presentInGlobalOverlay(contextController) controller.presentInGlobalOverlay(contextController)
} }
@ -630,28 +514,70 @@ final class GiftStoreScreenComponent: Component {
} }
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let searchQueryPromise = ValuePromise<String>("")
var items: [ContextMenuItem] = [] let attributes = self.state?.starGiftsState?.attributes ?? []
var allSelected = true let patternAttributes = attributes.filter { attribute in
if case .pattern = attribute {
var currentFilterAttributes: [ResaleGiftsContext.Attribute] = [] return true
var selectedIds = Set<Int64>() } else {
return false
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(.action(ContextMenuActionItem(text: "Select All", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? []
}, iconPosition: .left, action: { [weak self] _, f in let selectedPatternAttributes = currentFilterAttributes.filter { attribute in
f(.default) if case .pattern = attribute {
return true
if let self { } else {
return false
}
}
//TODO:localize
var items: [ContextMenuItem] = []
items.append(.custom(SearchContextItem(
context: component.context,
placeholder: "Search",
value: "",
valueChanged: { value in
searchQueryPromise.set(value)
}
), false))
items.append(.separator)
items.append(.custom(GiftAttributeListContextItem(
context: component.context,
attributes: patternAttributes,
selectedAttributes: selectedPatternAttributes,
attributeCount: self.state?.starGiftsState?.attributeCount ?? [:],
searchQuery: searchQueryPromise.get(),
attributeSelected: { [weak self] attribute, exclusive in
guard let self else {
return
}
var updatedFilterAttributes: [ResaleGiftsContext.Attribute]
if exclusive {
updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .pattern = attribute {
return false
}
return true
}
updatedFilterAttributes.append(attribute)
} else {
updatedFilterAttributes = currentFilterAttributes
if selectedPatternAttributes.contains(attribute) {
updatedFilterAttributes.removeAll(where: { $0 == attribute })
} else {
updatedFilterAttributes.append(attribute)
}
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
},
selectAll: { [weak self] in
guard let self else {
return
}
let updatedFilterAttributes = currentFilterAttributes.filter { attribute in let updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .pattern = attribute { if case .pattern = attribute {
return false return false
@ -660,65 +586,15 @@ final class GiftStoreScreenComponent: Component {
} }
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
} }
}))) ), false))
if let attributes = self.state?.starGiftsState?.attributes { let contextController = ContextController(
for attribute in attributes { context: component.context,
if case let .pattern(name, file, _) = attribute { presentationData: presentationData,
let isSelected = allSelected || selectedIds.contains(file.fileId.id) source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)),
items: .single(ContextController.Items(content: .list(items))),
var entities: [MessageTextEntity] = [] gesture: nil
var entityFiles: [Int64: TelegramMediaFile] = [:] )
entities = [
MessageTextEntity(
range: 0..<1,
type: .CustomEmoji(stickerPack: nil, fileId: file.fileId.id)
)
]
entityFiles[file.fileId.id] = file
var title = "# \(name)"
var count = ""
if let counter = self.state?.starGiftsState?.attributeCount[.pattern(file.fileId.id)] {
count = " \(presentationStringsFormattedNumber(counter, presentationData.dateTimeFormat.groupingSeparator))"
entities.append(
MessageTextEntity(range: title.count ..< title.count + count.count, type: .Bold)
)
title += count
}
items.append(.action(ContextMenuActionItem(text: title, entities: entities, entityFiles: entityFiles, enableEntityAnimations: false, icon: { theme in
return isSelected ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { [weak self] _, f in
f(.default)
if let self {
var updatedFilterAttributes = currentFilterAttributes
if selectedIds.contains(file.fileId.id) {
updatedFilterAttributes.removeAll(where: { $0 == .pattern(file.fileId.id) })
} else {
updatedFilterAttributes.append(.pattern(file.fileId.id))
}
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
}
}, longPressAction: { [weak self] _, f in
f(.default)
if let self {
var updatedFilterAttributes = currentFilterAttributes.filter { attribute in
if case .pattern = attribute {
return false
}
return true
}
updatedFilterAttributes.append(.pattern(file.fileId.id))
self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes)
}
})))
}
}
}
let contextController = ContextController(context: component.context, presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
controller.presentInGlobalOverlay(contextController) controller.presentInGlobalOverlay(contextController)
} }
@ -729,7 +605,6 @@ final class GiftStoreScreenComponent: Component {
} }
let environment = environment[EnvironmentType.self].value let environment = environment[EnvironmentType.self].value
let controller = environment.controller
let themeUpdated = self.environment?.theme !== environment.theme let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment self.environment = environment
self.state = state 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)) 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 let topPanelView = self.topPanel.view, let topSeparatorView = self.topSeparator.view {
if topPanelView.superview == nil { if topPanelView.superview == nil {
topPanelView.alpha = 0.0
topSeparatorView.alpha = 0.0
self.addSubview(topPanelView) self.addSubview(topPanelView)
self.addSubview(topSeparatorView) self.addSubview(topSeparatorView)
} }
@ -792,51 +670,19 @@ final class GiftStoreScreenComponent: Component {
transition.setFrame(view: topSeparatorView, frame: topSeparatorFrame) transition.setFrame(view: topSeparatorView, frame: topSeparatorFrame)
} }
let cancelButtonSize = self.cancelButton.update( // 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(
// transition: transition, // transition: transition,
// component: AnyComponent( // component: AnyComponent(
// PlainButtonComponent( // PlainButtonComponent(
// content: AnyComponent( // content: AnyComponent(
// BundleIconComponent( // MultilineTextComponent(
// name: "Peer Info/SortIcon", // text: .plain(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor)),
// tintColor: theme.rootController.navigationBar.accentTextColor // horizontalAlignment: .center
// ) // )
// ), // ),
// effectAlignment: .center, // effectAlignment: .center,
// action: { [weak self] in // action: {
// if let sourceView = self?.sortButton.view { // controller()?.dismiss()
// self?.openContextMenu(sourceView: sourceView)
// }
// }, // },
// animateScale: false // animateScale: false
// ) // )
@ -844,15 +690,14 @@ final class GiftStoreScreenComponent: Component {
// environment: {}, // environment: {},
// containerSize: CGSize(width: availableSize.width, height: 100.0) // 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) // 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 sortButtonView = self.sortButton.view { // if let cancelButtonView = self.cancelButton.view {
// if sortButtonView.superview == nil { // if cancelButtonView.superview == nil {
// self.addSubview(sortButtonView) // self.addSubview(cancelButtonView)
// } // }
// transition.setFrame(view: sortButtonView, frame: sortButtonFrame) // transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame)
// transition.setAlpha(view: sortButtonView, alpha: showFilters ? 0.0 : 1.0)
// } // }
let balanceTitleSize = self.balanceTitle.update( let balanceTitleSize = self.balanceTitle.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(MultilineTextComponent( component: AnyComponent(MultilineTextComponent(
@ -902,16 +747,12 @@ final class GiftStoreScreenComponent: Component {
balanceValueView.bounds = CGRect(origin: .zero, size: balanceValueSize) 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.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) 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( let titleSize = self.title.update(
transition: transition, transition: transition,
component: AnyComponent(MultilineTextComponent( 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 horizontalAlignment: .center
)), )),
environment: {}, 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)) 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( let subtitleSize = self.subtitle.update(
transition: transition, transition: transition,
component: AnyComponent(BalancedTextComponent( 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, horizontalAlignment: .center,
maximumNumberOfLines: 1 maximumNumberOfLines: 1
)), )),
@ -946,20 +796,25 @@ final class GiftStoreScreenComponent: Component {
let optionWidth = (availableSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0 let optionWidth = (availableSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
var sortingTitle = "Date" var sortingTitle = "Date"
var sortingIcon: String = "Peer Info/SortDate"
if let sorting = self.state?.starGiftsState?.sorting { if let sorting = self.state?.starGiftsState?.sorting {
switch sorting { switch sorting {
case .date: case .date:
sortingTitle = "Date" sortingTitle = "Date"
sortingIcon = "Peer Info/SortDate"
case .value: case .value:
sortingTitle = "Price" sortingTitle = "Price"
sortingIcon = "Peer Info/SortValue"
case .number: case .number:
sortingTitle = "Number" sortingTitle = "Number"
sortingIcon = "Peer Info/SortNumber"
} }
} }
var filterItems: [FilterSelectorComponent.Item] = [] var filterItems: [FilterSelectorComponent.Item] = []
filterItems.append(FilterSelectorComponent.Item( filterItems.append(FilterSelectorComponent.Item(
id: AnyHashable(0), id: AnyHashable(0),
iconName: sortingIcon,
title: sortingTitle, title: sortingTitle,
action: { [weak self] view in action: { [weak self] view in
if let self { if let self {
@ -1043,7 +898,7 @@ final class GiftStoreScreenComponent: Component {
component: AnyComponent(FilterSelectorComponent( component: AnyComponent(FilterSelectorComponent(
context: component.context, context: component.context,
colors: FilterSelectorComponent.Colors( colors: FilterSelectorComponent.Colors(
foreground: theme.list.itemSecondaryTextColor, foreground: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.65),
background: theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15) background: theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15)
), ),
items: filterItems items: filterItems
@ -1081,7 +936,7 @@ final class GiftStoreScreenComponent: Component {
self.scrollView.contentSize = contentSize self.scrollView.contentSize = contentSize
self.nextScrollTransition = nil 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 { if self.scrollView.scrollIndicatorInsets != scrollInsets {
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)) 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) 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 sideInset: CGFloat = 44.0
let emptyAnimationHeight = 148.0 let emptyAnimationHeight = 148.0
let topInset: CGFloat = environment.navigationHeight + 39.0 let topInset: CGFloat = environment.navigationHeight + 39.0
@ -1149,10 +1004,7 @@ final class GiftStoreScreenComponent: Component {
guard let self else { guard let self else {
return return
} }
self.selectedModels.removeAll() self.state?.starGiftsContext.updateFilterAttributes([])
self.selectedBackdrops.removeAll()
self.selectedSymbols.removeAll()
self.simulateLoading()
}, },
animateScale: false animateScale: false
) )
@ -1205,6 +1057,8 @@ final class GiftStoreScreenComponent: Component {
} }
view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size) view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size)
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsActionFrame.center) ComponentTransition.immediate.setPosition(view: view, position: emptyResultsActionFrame.center)
view.alpha = self.state?.starGiftsState?.attributes.isEmpty == true ? 0.0 : 1.0
} }
} else { } else {
if let view = self.emptyResultsAnimation.view { if let view = self.emptyResultsAnimation.view {
@ -1234,6 +1088,7 @@ final class GiftStoreScreenComponent: Component {
final class State: ComponentState { final class State: ComponentState {
private let context: AccountContext private let context: AccountContext
var peerId: EnginePeer.Id
private var disposable: Disposable? private var disposable: Disposable?
fileprivate let starGiftsContext: ResaleGiftsContext fileprivate let starGiftsContext: ResaleGiftsContext
@ -1241,9 +1096,11 @@ final class GiftStoreScreenComponent: Component {
init( init(
context: AccountContext, context: AccountContext,
peerId: EnginePeer.Id,
giftId: Int64 giftId: Int64
) { ) {
self.context = context self.context = context
self.peerId = peerId
self.starGiftsContext = ResaleGiftsContext(account: context.account, giftId: giftId) self.starGiftsContext = ResaleGiftsContext(account: context.account, giftId: giftId)
super.init() super.init()
@ -1264,7 +1121,7 @@ final class GiftStoreScreenComponent: Component {
} }
func makeState() -> State { 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 { func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
@ -1292,11 +1149,10 @@ public class GiftStoreScreen: ViewControllerComponentContainer {
starsContext: starsContext, starsContext: starsContext,
peerId: peerId, peerId: peerId,
gift: gift 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.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.context.sharedContext.currentPresentationData.with { $0 }.strings.Common_Back, style: .plain, target: nil, action: nil)
self.scrollToTop = { [weak self] in self.scrollToTop = { [weak self] in
guard let self, let componentView = self.node.hostView.componentView as? GiftStoreScreenComponent.View else { guard let self, let componentView = self.node.hostView.componentView as? GiftStoreScreenComponent.View else {
return return
@ -1332,6 +1188,8 @@ private final class GiftStoreReferenceContentSource: ContextReferenceContentSour
private let controller: ViewController private let controller: ViewController
private let sourceView: UIView private let sourceView: UIView
let forceDisplayBelowKeyboard = true
init(controller: ViewController, sourceView: UIView) { init(controller: ViewController, sourceView: UIView) {
self.controller = controller self.controller = controller
self.sourceView = sourceView self.sourceView = sourceView

View File

@ -134,6 +134,7 @@ final class LoadingShimmerNode: ASDisplayNode {
super.init() super.init()
self.allowsGroupOpacity = true
self.isUserInteractionEnabled = false self.isUserInteractionEnabled = false
self.addSubnode(self.backgroundColorNode) 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 AccountContext
import TelegramPresentationData 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 { final class ButtonsComponent: Component {
let theme: PresentationTheme let theme: PresentationTheme
let isOverlay: Bool let isOverlay: Bool

View File

@ -2010,7 +2010,8 @@ final class MediaEditorScreenComponent: Component {
self.isSelectionPanelOpen = !self.isSelectionPanelOpen self.isSelectionPanelOpen = !self.isSelectionPanelOpen
self.state?.updated() self.state?.updated()
} }
} },
animateAlpha: false
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: 33.0, height: 33.0) 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) 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 shareStory: { [weak self] uniqueGift in
guard let self, let controller = self.controller else { guard let self, let controller = self.controller else {
return return
@ -11118,7 +11124,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
giftsContext?.updateSorting(sorting == .date ? .value : .date) giftsContext?.updateSorting(sorting == .date ? .value : .date)
}))) })))
if hasPinnedGifts { if hasPinnedGifts && hasVisibility {
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Reorder, icon: { theme in 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) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor)
}, action: { _, f in }, 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) 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 updateResellStars: { [weak self] price in
guard let self, case let .unique(uniqueGift) = product.gift else { guard let self, case let .unique(uniqueGift) = product.gift else {
return return

View File

@ -147,9 +147,9 @@ private final class SheetContent: CombinedComponent {
minAmount = StarsAmount(value: 1, nanos: 0) minAmount = StarsAmount(value: 1, nanos: 0)
maxAmount = withdrawConfiguration.maxPaidMediaAmount.flatMap { StarsAmount(value: $0, nanos: 0) } maxAmount = withdrawConfiguration.maxPaidMediaAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
amountLabel = nil amountLabel = nil
case .starGiftResell: case let .starGiftResell(update):
//TODO:localize //TODO:localize
titleString = "Sell Gift" titleString = update ? "Edit Price" : "Sell Gift"
amountTitle = "PRICE IN STARS" amountTitle = "PRICE IN STARS"
amountPlaceholder = "Enter Price" amountPlaceholder = "Enter Price"
@ -358,12 +358,6 @@ private final class SheetContent: CombinedComponent {
buttonAttributedString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: buttonAttributedString.string)) 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( let button = button.update(
component: ButtonComponent( component: ButtonComponent(
background: ButtonComponent.Background( background: ButtonComponent.Background(
@ -558,10 +552,11 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
case accountWithdraw case accountWithdraw
case paidMedia(Int64?) case paidMedia(Int64?)
case reaction(Int64?) case reaction(Int64?)
case starGiftResell case starGiftResell(Bool)
} }
private let context: AccountContext private let context: AccountContext
private let mode: StarsWithdrawScreen.Mode
fileprivate let completion: (Int64) -> Void fileprivate let completion: (Int64) -> Void
public init( public init(
@ -570,6 +565,7 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
completion: @escaping (Int64) -> Void completion: @escaping (Int64) -> Void
) { ) {
self.context = context self.context = context
self.mode = mode
self.completion = completion self.completion = completion
super.init( super.init(
@ -603,12 +599,17 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
func presentMinAmountTooltip(_ minAmount: Int64) { func presentMinAmountTooltip(_ minAmount: Int64) {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } 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( let resultController = UndoOverlayController(
presentationData: presentationData, presentationData: presentationData,
content: .image( content: .image(
image: UIImage(bundleImageName: "Premium/Stars/StarLarge")!, image: UIImage(bundleImageName: "Premium/Stars/StarLarge")!,
title: nil, title: nil,
text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum(presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum_Stars(Int32(minAmount))).string, text: text,
round: false, round: false,
undoText: nil 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 controller.navigationPresentation = .modal
let _ = combineLatest( let _ = (combineLatest(
queue: Queue.mainQueue(), queue: Queue.mainQueue(),
controller.result, controller.result,
options.get() options.get())
).startStandalone(next: { [weak controller] result, options in |> 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 let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer, let starsContext = context.starsContext {
if case .starGiftTransfer = source { if case .starGiftTransfer = source {
presentTransferAlertImpl?(EnginePeer(peer)) presentTransferAlertImpl?(EnginePeer(peer))
@ -3275,7 +3275,6 @@ public final class SharedAccountContextImpl: SharedAccountContext {
fatalError() fatalError()
} }
let controller = GiftStoreScreen(context: context, starsContext: starsContext, peerId: peerId, gift: gift) let controller = GiftStoreScreen(context: context, starsContext: starsContext, peerId: peerId, gift: gift)
controller.navigationPresentation = .modal
return controller return controller
} }
@ -3668,8 +3667,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return StarsWithdrawScreen(context: context, mode: .accountWithdraw, completion: completion) return StarsWithdrawScreen(context: context, mode: .accountWithdraw, completion: completion)
} }
public func makeStarGiftResellScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController { public func makeStarGiftResellScreen(context: AccountContext, update: Bool, completion: @escaping (Int64) -> Void) -> ViewController {
return StarsWithdrawScreen(context: context, mode: .starGiftResell, completion: completion) return StarsWithdrawScreen(context: context, mode: .starGiftResell(update), completion: completion)
} }
public func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController { 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 { 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 = { controller.disposed = {
dismissed?() dismissed?()
} }