diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 4ef8edafd0..3e353b4b4e 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1178,7 +1178,7 @@ public protocol SharedAccountContext: AnyObject { func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsWithdrawalScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController - func makeStarGiftResellScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController + func makeStarGiftResellScreen(context: AccountContext, update: Bool, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> ViewController func makeStarsIntroScreen(context: AccountContext) -> ViewController diff --git a/submodules/AccountContext/Sources/AttachmentMainButtonState.swift b/submodules/AccountContext/Sources/AttachmentMainButtonState.swift index 49650fca00..58de64007d 100644 --- a/submodules/AccountContext/Sources/AttachmentMainButtonState.swift +++ b/submodules/AccountContext/Sources/AttachmentMainButtonState.swift @@ -41,6 +41,7 @@ public struct AttachmentMainButtonState { public let progress: Progress public let isEnabled: Bool public let hasShimmer: Bool + public let iconName: String? public let position: Position? public init( @@ -53,6 +54,7 @@ public struct AttachmentMainButtonState { progress: Progress, isEnabled: Bool, hasShimmer: Bool, + iconName: String? = nil, position: Position? = nil ) { self.text = text @@ -64,6 +66,7 @@ public struct AttachmentMainButtonState { self.progress = progress self.isEnabled = isEnabled self.hasShimmer = hasShimmer + self.iconName = iconName self.position = position } diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index fceecdd61b..b9e12076ff 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -476,6 +476,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode { private var size: CGSize? private let backgroundAnimationNode: ASImageNode + private var iconNode: ASImageNode? fileprivate let textNode: ImmediateTextNode private var badgeNode: BadgeNode? private let statusNode: SemanticStatusNode @@ -781,6 +782,25 @@ private final class MainButtonNode: HighlightTrackingButtonNode { badgeNode.removeFromSupernode() } + if let iconName = state.iconName { + let iconNode: ASImageNode + if let current = self.iconNode { + iconNode = current + } else { + iconNode = ASImageNode() + iconNode.displaysAsynchronously = false + iconNode.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: state.textColor) + self.addSubnode(iconNode) + } + if let iconSize = iconNode.image?.size { + textFrame.origin.x += (iconSize.width + 6.0) / 2.0 + iconNode.frame = CGRect(origin: CGPoint(x: textFrame.minX - iconSize.width - 6.0, y: textFrame.minY + floorToScreenPixels((textFrame.height - iconSize.height) * 0.5)), size: iconSize) + } + } else if let iconNode = self.iconNode { + self.iconNode = nil + iconNode.removeFromSupernode() + } + if self.textNode.frame.width.isZero { self.textNode.frame = textFrame } else { @@ -795,7 +815,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode { self.transitionFromProgress() } } - + if let shimmerView = self.shimmerView, let borderView = self.borderView, let borderMaskView = self.borderMaskView, let borderShimmerView = self.borderShimmerView { let buttonFrame = CGRect(origin: .zero, size: size) let buttonWidth = size.width diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index dd55c805f3..6842c65076 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -2145,6 +2145,8 @@ public protocol ContextReferenceContentSource: AnyObject { var shouldBeDismissed: Signal { get } + var forceDisplayBelowKeyboard: Bool { get } + func transitionInfo() -> ContextControllerReferenceViewInfo? } @@ -2153,6 +2155,10 @@ public extension ContextReferenceContentSource { return false } + var forceDisplayBelowKeyboard: Bool { + return false + } + var shouldBeDismissed: Signal { return .single(false) } @@ -2744,7 +2750,9 @@ public final class ContextController: ViewController, StandalonePresentableContr } public func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?) { - if viewTreeContainsFirstResponder(view: self.view) { + if let mainSource = self.configuration.sources.first(where: { $0.id == self.configuration.initialId }), case let .reference(source) = mainSource.source, source.forceDisplayBelowKeyboard { + + } else if viewTreeContainsFirstResponder(view: self.view) { self.dismissOnInputClose = (result, completion) self.view.endEditing(true) return diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index e26bf1de82..7436c56971 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -273,7 +273,7 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking return super.hitTest(point, with: event) } - func setItem(item: ContextMenuActionItem) { + public func setItem(item: ContextMenuActionItem) { self.item = item self.accessibilityLabel = item.text } @@ -363,6 +363,8 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking return ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: fileId, enableAnimation: true), range: entity.range) } else if case .Bold = entity.type { return ChatTextInputStateTextAttribute(type: .bold, range: entity.range) + } else if case .Italic = entity.type { + return ChatTextInputStateTextAttribute(type: .italic, range: entity.range) } return nil }) @@ -373,6 +375,8 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking ], range: NSRange(location: 0, length: result.length)) for attribute in inputStateText.attributes { if case .bold = attribute.type { + result.addAttribute(NSAttributedString.Key.font, value: Font.semibold(presentationData.listsFontSize.baseDisplaySize), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) + } else if case .italic = attribute.type { result.addAttribute(NSAttributedString.Key.font, value: Font.semibold(15.0), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) } } diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 5cd59d6571..c79048b077 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -500,6 +500,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo func wantsDisplayBelowKeyboard() -> Bool { if let reactionContextNode = self.reactionContextNode { return reactionContextNode.wantsDisplayBelowKeyboard() + } else if case let .reference(source) = self.source { + return source.forceDisplayBelowKeyboard } else { return false } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 8a64406ecd..6d13bbbcb4 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -1823,6 +1823,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att private var isDismissing = false fileprivate let mainButtonStatePromise = Promise(nil) + fileprivate let secondaryButtonStatePromise = Promise(nil) private let mainButtonAction: (() -> Void)? @@ -2380,9 +2381,23 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att transition.updateAlpha(node: self.moreButtonNode.iconNode, alpha: moreIsVisible ? 1.0 : 0.0) transition.updateTransformScale(node: self.moreButtonNode.iconNode, scale: moreIsVisible ? 1.0 : 0.1) - //if self. { - //self.mainButtonStatePromise.set(.single(AttachmentMainButtonState(text: "Add", badge: "\(count)", font: .bold, background: .color(self.presentationData.theme.actionSheet.controlAccentColor), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, isVisible: count > 0, progress: .none, isEnabled: true, hasShimmer: false))) - //} + if self.selectionCount > 0 { + //TODO:localize + var text = "Create 1 Story" + if self.selectionCount > 1 { + text = "Create \(self.selectionCount) Stories" + } + self.mainButtonStatePromise.set(.single(AttachmentMainButtonState(text: text, badge: nil, font: .bold, background: .color(self.presentationData.theme.actionSheet.controlAccentColor), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, position: .top))) + + if self.selectionCount > 1 && self.selectionCount <= 6 { + self.secondaryButtonStatePromise.set(.single(AttachmentMainButtonState(text: "Combine into Collage", badge: nil, font: .regular, background: .color(.clear), textColor: self.presentationData.theme.actionSheet.controlAccentColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, iconName: "Media Editor/Collage", position: .bottom))) + } else { + self.secondaryButtonStatePromise.set(.single(nil)) + } + } else { + self.mainButtonStatePromise.set(.single(nil)) + self.secondaryButtonStatePromise.set(.single(nil)) + } } private func updateThemeAndStrings() { @@ -2933,6 +2948,10 @@ final class MediaPickerContext: AttachmentMediaPickerContext { return self.controller?.mainButtonStatePromise.get() ?? .single(nil) } + public var secondaryButtonState: Signal { + return self.controller?.secondaryButtonStatePromise.get() ?? .single(nil) + } + init(controller: MediaPickerScreenImpl) { self.controller = controller } @@ -2952,6 +2971,10 @@ final class MediaPickerContext: AttachmentMediaPickerContext { func mainButtonAction() { self.controller?.mainButtonPressed() } + + func secondaryButtonAction() { + self.controller?.mainButtonPressed() + } } private final class MediaPickerContextReferenceContentSource: ContextReferenceContentSource { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 20e098199c..5bb4534ccd 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -1459,23 +1459,26 @@ private final class ProfileGiftsContextImpl { return _internal_transferStarGift(account: self.account, prepaid: prepaid, reference: reference, peerId: peerId) } - func buyStarGift(gift inputGift: TelegramCore.StarGift, peerId: EnginePeer.Id) -> Signal { - var gift = self.gifts.first(where: { $0.gift == inputGift }) - if gift == nil { - gift = self.filteredGifts.first(where: { $0.gift == inputGift }) - } - guard case let .unique(uniqueGift) = gift?.gift else { - return .complete() - } - + func buyStarGift(slug: String, peerId: EnginePeer.Id) -> Signal { if let count = self.count { self.count = max(0, count - 1) } - self.gifts.removeAll(where: { $0.gift == inputGift }) - self.filteredGifts.removeAll(where: { $0.gift == inputGift }) + self.gifts.removeAll(where: { gift in + if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug { + return true + } + return false + }) + self.filteredGifts.removeAll(where: { gift in + if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug { + return true + } + return false + }) + self.pushState() - return _internal_buyStarGift(account: self.account, slug: uniqueGift.slug, peerId: peerId) + return _internal_buyStarGift(account: self.account, slug: slug, peerId: peerId) } func removeStarGift(gift: TelegramCore.StarGift) { @@ -1899,11 +1902,11 @@ public final class ProfileGiftsContext { } } - public func buyStarGift(gift: TelegramCore.StarGift, peerId: EnginePeer.Id) -> Signal { + public func buyStarGift(slug: String, peerId: EnginePeer.Id) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in - disposable.set(impl.buyStarGift(gift: gift, peerId: peerId).start(error: { error in + disposable.set(impl.buyStarGift(slug: slug, peerId: peerId).start(error: { error in subscriber.putError(error) }, completed: { subscriber.putCompletion() @@ -2308,7 +2311,7 @@ private final class ResaleGiftsContextImpl { private let disposable = MetaDisposable() - private var sorting: ResaleGiftsContext.Sorting = .date + private var sorting: ResaleGiftsContext.Sorting = .value private var filterAttributes: [ResaleGiftsContext.Attribute] = [] private var gifts: [StarGift] = [] @@ -2431,7 +2434,15 @@ private final class ResaleGiftsContextImpl { let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) - return (gifts.compactMap { StarGift(apiStarGift: $0) }, resultAttributes, attributeCount, count, nextOffset) + + var mappedGifts: [StarGift] = [] + for gift in gifts { + if let mappedGift = StarGift(apiStarGift: gift), case let .unique(uniqueGift) = mappedGift, let resellStars = uniqueGift.resellStars, resellStars > 0 { + mappedGifts.append(mappedGift) + } + } + + return (mappedGifts, resultAttributes, attributeCount, count, nextOffset) } } } @@ -2444,9 +2455,7 @@ private final class ResaleGiftsContextImpl { if initialNextOffset == nil || reload { self.gifts = gifts } else { - for gift in gifts { - self.gifts.append(gift) - } + self.gifts.append(contentsOf: gifts) } let updatedCount = max(Int32(self.gifts.count), count) @@ -2473,6 +2482,11 @@ private final class ResaleGiftsContextImpl { self.loadMore() } + func removeStarGift(gift: TelegramCore.StarGift) { + self.gifts.removeAll(where: { $0 == gift }) + self.pushState() + } + func updateSorting(_ sorting: ResaleGiftsContext.Sorting) { guard self.sorting != sorting else { return @@ -2571,6 +2585,12 @@ public final class ResaleGiftsContext { impl.updateFilterAttributes(attributes) } } + + public func removeStarGift(gift: TelegramCore.StarGift) { + self.impl.with { impl in + impl.removeStarGift(gift: gift) + } + } public var currentState: ResaleGiftsContext.State? { var state: ResaleGiftsContext.State? diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 4207c622b2..4677f08a9e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -125,7 +125,7 @@ public extension TelegramEngine { return _internal_transferStarGift(account: self.account, prepaid: prepaid, reference: reference, peerId: peerId) } - public func buyStarGift(prepaid: Bool, slug: String, peerId: EnginePeer.Id) -> Signal { + public func buyStarGift(slug: String, peerId: EnginePeer.Id) -> Signal { return _internal_buyStarGift(account: self.account, slug: slug, peerId: peerId) } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index f3cec6741b..c612e59a65 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -2726,9 +2726,13 @@ public class CameraScreenImpl: ViewController, CameraScreen { self.additionalPreviewView.isEnabled = false self.collageView?.isEnabled = false + #if targetEnvironment(simulator) + + #else Queue.mainQueue().after(0.3) { self.previewBlurPromise.set(true) } + #endif self.camera?.stopCapture() self.cameraIsActive = false @@ -3627,12 +3631,17 @@ public class CameraScreenImpl: ViewController, CameraScreen { if self.cameraState.isCollageEnabled, let collage = self.node.collage { selectionLimit = collage.grid.count - collage.results.count } else { - selectionLimit = 6 + if self.cameraState.isCollageEnabled { + selectionLimit = 6 + } else { + selectionLimit = 10 + } } + //TODO:unmock controller = self.context.sharedContext.makeStoryMediaPickerScreen( context: self.context, isDark: true, - forCollage: self.cameraState.isCollageEnabled, + forCollage: self.cameraState.isCollageEnabled || "".isEmpty, selectionLimit: selectionLimit, getSourceRect: { [weak self] in if let self { diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD index 213684cbca..60e90c4299 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD @@ -44,6 +44,7 @@ swift_library( "//submodules/TelegramUI/Components/Gifts/GiftSetupScreen", "//submodules/TelegramUI/Components/Gifts/GiftViewScreen", "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/TextFieldComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/FiltersComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/FilterSelectorComponent.swift similarity index 87% rename from submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/FiltersComponent.swift rename to submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/FilterSelectorComponent.swift index eb8bff0a73..5b31427f89 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/FiltersComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/FilterSelectorComponent.swift @@ -24,21 +24,24 @@ public final class FilterSelectorComponent: Component { public struct Item: Equatable { public var id: AnyHashable + public var iconName: String? public var title: String public var action: (UIView) -> Void public init( id: AnyHashable, + iconName: String? = nil, title: String, action: @escaping (UIView) -> Void ) { self.id = id + self.iconName = iconName self.title = title self.action = action } public static func ==(lhs: Item, rhs: Item) -> Bool { - return lhs.id == rhs.id && lhs.title == rhs.title + return lhs.id == rhs.id && lhs.iconName == rhs.iconName && lhs.title == rhs.title } } @@ -142,6 +145,7 @@ public final class FilterSelectorComponent: Component { component: AnyComponent(PlainButtonComponent( content: AnyComponent(ItemComponent( context: component.context, + iconName: item.iconName, text: item.title, font: itemFont, color: component.colors.foreground, @@ -231,6 +235,7 @@ extension CGRect { private final class ItemComponent: CombinedComponent { let context: AccountContext? + let iconName: String? let text: String let font: UIFont let color: UIColor @@ -238,12 +243,14 @@ private final class ItemComponent: CombinedComponent { init( context: AccountContext?, + iconName: String?, text: String, font: UIFont, color: UIColor, backgroundColor: UIColor ) { self.context = context + self.iconName = iconName self.text = text self.font = font self.color = color @@ -254,6 +261,9 @@ private final class ItemComponent: CombinedComponent { if lhs.context !== rhs.context { return false } + if lhs.iconName != rhs.iconName { + return false + } if lhs.text != rhs.text { return false } @@ -297,17 +307,22 @@ private final class ItemComponent: CombinedComponent { let icon = icon.update( component: BundleIconComponent( - name: "Item List/ExpandableSelectorArrows", - tintColor: component.color + name: component.iconName ?? "Item List/ExpandableSelectorArrows", + tintColor: component.color, + maxSize: component.iconName != nil ? CGSize(width: 22.0, height: 22.0) : nil ), availableSize: CGSize(width: 100, height: 100), transition: .immediate ) let padding: CGFloat = 12.0 + var leftPadding = padding + if let _ = component.iconName { + leftPadding -= 4.0 + } let spacing: CGFloat = 4.0 let totalWidth = title.size.width + icon.size.width + spacing - let size = CGSize(width: totalWidth + padding * 2.0, height: 28.0) + let size = CGSize(width: totalWidth + leftPadding + padding, height: 28.0) let background = background.update( component: RoundedRectangle( color: component.backgroundColor, @@ -319,12 +334,21 @@ private final class ItemComponent: CombinedComponent { context.add(background .position(CGPoint(x: size.width / 2.0, y: size.height / 2.0)) ) - context.add(title - .position(CGPoint(x: padding + title.size.width / 2.0, y: size.height / 2.0)) - ) - context.add(icon - .position(CGPoint(x: size.width - padding - icon.size.width / 2.0, y: size.height / 2.0)) - ) + if let _ = component.iconName { + context.add(title + .position(CGPoint(x: size.width - padding - title.size.width / 2.0, y: size.height / 2.0)) + ) + context.add(icon + .position(CGPoint(x: leftPadding + icon.size.width / 2.0, y: size.height / 2.0)) + ) + } else { + context.add(title + .position(CGPoint(x: padding + title.size.width / 2.0, y: size.height / 2.0)) + ) + context.add(icon + .position(CGPoint(x: size.width - padding - icon.size.width / 2.0, y: size.height / 2.0)) + ) + } return size } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift new file mode 100644 index 0000000000..a55122afa6 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift @@ -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 + 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, + 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, 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.. 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() + 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 +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift index 75d49243ff..46fee26be0 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift @@ -88,7 +88,6 @@ final class GiftStoreScreenComponent: Component { private var starsItems: [AnyHashable: ComponentView] = [:] private let filterSelector = ComponentView() - private var isLoading = false private var isUpdating: Bool = false @@ -144,48 +143,13 @@ final class GiftStoreScreenComponent: Component { private var effectiveGifts: [StarGift]? { if let gifts = self.state?.starGiftsState?.gifts { return gifts -// if self.selectedModels.isEmpty && self.selectedBackdrops.isEmpty && self.selectedSymbols.isEmpty { -// return gifts -// } else if let (currentGifts, currentModels, currentBackdrops, currentSymbols) = self.currentGifts, currentModels == self.selectedModels && currentBackdrops == self.selectedBackdrops && currentSymbols == self.selectedSymbols { -// return currentGifts -// } else { -// var filteredGifts: [StarGift] = [] -// for gift in gifts { -// guard case let .unique(uniqueGift) = gift else { -// continue -// } -// var match = true -// for attribute in uniqueGift.attributes { -// if case let .model(name, _, _) = attribute { -// if !self.selectedModels.isEmpty && !self.selectedModels.contains(name) { -// match = false -// } -// } -// if case let .backdrop(name, _, _, _, _, _, _) = attribute { -// if !self.selectedBackdrops.isEmpty && !self.selectedBackdrops.contains(name) { -// match = false -// } -// } -// if case let .pattern(name, _, _) = attribute { -// if !self.selectedSymbols.isEmpty && !self.selectedSymbols.contains(name) { -// match = false -// } -// } -// } -// if match { -// filteredGifts.append(gift) -// } -// } -// self.currentGifts = (filteredGifts, self.selectedModels, self.selectedBackdrops, self.selectedSymbols) -// return filteredGifts -// } } else { return nil } } private func updateScrolling(interactive: Bool = false, transition: ComponentTransition) { - guard let environment = self.environment, let component = self.component, !self.isLoading else { + guard let environment = self.environment, let component = self.component, self.state?.starGiftsState?.dataState != .loading else { return } @@ -267,7 +231,7 @@ final class GiftStoreScreenComponent: Component { ), effectAlignment: .center, action: { [weak self] in - if let self, let component = self.component { + if let self, let component = self.component, let state = self.state { if let controller = controller() as? GiftStoreScreen { let mainController: ViewController if let parentController = controller.parentController() { @@ -277,7 +241,7 @@ final class GiftStoreScreenComponent: Component { } let giftController = GiftViewScreen( context: component.context, - subject: .uniqueGift(uniqueGift) + subject: .uniqueGift(uniqueGift, state.peerId) ) mainController.push(giftController) } @@ -330,77 +294,6 @@ final class GiftStoreScreenComponent: Component { } } - var selectedModels = Set() - var selectedBackdrops = Set() - var selectedSymbols = Set() - - private func simulateLoading() { - self.isLoading = true - self.state?.updated(transition: .immediate) - - Queue.mainQueue().after(1.0, { - self.isLoading = false - self.state?.updated(transition: .immediate) - }) - } - - func openContextMenu(sourceView: UIView) { - guard let component = self.component, let controller = self.environment?.controller() else { - return - } - - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - - var items: [ContextMenuItem] = [] - items.append(.action(ContextMenuActionItem(text: "Sort by Price", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortValue"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, f in - f(.default) - - self?.state?.starGiftsContext.updateSorting(.value) - }))) - items.append(.action(ContextMenuActionItem(text: "Sort by Date", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortDate"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, f in - f(.default) - - self?.state?.starGiftsContext.updateSorting(.date) - }))) - items.append(.action(ContextMenuActionItem(text: "Sort by Number", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortNumber"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, f in - f(.default) - - self?.state?.starGiftsContext.updateSorting(.number) - }))) - - items.append(.separator) - - items.append(.action(ContextMenuActionItem(text: "Model", textLayout: .secondLineWithValue("all models"), icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortNumber"), color: theme.contextMenu.primaryColor) - }, action: { _, f in - f(.default) - - }))) - - items.append(.action(ContextMenuActionItem(text: "Backdrop", textLayout: .secondLineWithValue("all backdrops"), icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortNumber"), color: theme.contextMenu.primaryColor) - }, action: { _, f in - f(.default) - - }))) - - items.append(.action(ContextMenuActionItem(text: "Symbol", textLayout: .secondLineWithValue("all symbols"), icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortNumber"), color: theme.contextMenu.primaryColor) - }, action: { _, f in - f(.default) - - }))) - - let contextController = ContextController(context: component.context, presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) - controller.presentInGlobalOverlay(contextController) - } - func openSortContextMenu(sourceView: UIView) { guard let component = self.component, let controller = self.environment?.controller() else { return @@ -441,28 +334,70 @@ final class GiftStoreScreenComponent: Component { } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let searchQueryPromise = ValuePromise("") - var items: [ContextMenuItem] = [] - var allSelected = true - - var currentFilterAttributes: [ResaleGiftsContext.Attribute] = [] - var selectedIds = Set() - - if let filterAttributes = self.state?.starGiftsState?.filterAttributes { - currentFilterAttributes = filterAttributes - for attribute in filterAttributes { - if case let .model(id) = attribute { - allSelected = false - selectedIds.insert(id) - } + let attributes = self.state?.starGiftsState?.attributes ?? [] + let modelAttributes = attributes.filter { attribute in + if case .model = attribute { + return true + } else { + return false } } - items.append(.action(ContextMenuActionItem(text: "Select All", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) - }, iconPosition: .left, action: { [weak self] _, f in - f(.default) - - if let self { + + let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? [] + let selectedModelAttributes = currentFilterAttributes.filter { attribute in + if case .model = attribute { + return true + } else { + return false + } + } + + //TODO:localize + var items: [ContextMenuItem] = [] + items.append(.custom(SearchContextItem( + context: component.context, + placeholder: "Search", + value: "", + valueChanged: { value in + searchQueryPromise.set(value) + } + ), false)) + items.append(.separator) + items.append(.custom(GiftAttributeListContextItem( + context: component.context, + attributes: modelAttributes, + selectedAttributes: selectedModelAttributes, + attributeCount: self.state?.starGiftsState?.attributeCount ?? [:], + searchQuery: searchQueryPromise.get(), + attributeSelected: { [weak self] attribute, exclusive in + guard let self else { + return + } + var updatedFilterAttributes: [ResaleGiftsContext.Attribute] + if exclusive { + updatedFilterAttributes = currentFilterAttributes.filter { attribute in + if case .model = attribute { + return false + } + return true + } + updatedFilterAttributes.append(attribute) + } else { + updatedFilterAttributes = currentFilterAttributes + if selectedModelAttributes.contains(attribute) { + updatedFilterAttributes.removeAll(where: { $0 == attribute }) + } else { + updatedFilterAttributes.append(attribute) + } + } + self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) + }, + selectAll: { [weak self] in + guard let self else { + return + } let updatedFilterAttributes = currentFilterAttributes.filter { attribute in if case .model = attribute { return false @@ -471,65 +406,15 @@ final class GiftStoreScreenComponent: Component { } self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) } - }))) + ), false)) - if let attributes = self.state?.starGiftsState?.attributes { - for attribute in attributes { - if case let .model(name, file, _) = attribute { - let isSelected = allSelected || selectedIds.contains(file.fileId.id) - - var entities: [MessageTextEntity] = [] - var entityFiles: [Int64: TelegramMediaFile] = [:] - entities = [ - MessageTextEntity( - range: 0..<1, - type: .CustomEmoji(stickerPack: nil, fileId: file.fileId.id) - ) - ] - entityFiles[file.fileId.id] = file - - var title = "# \(name)" - var count = "" - if let counter = self.state?.starGiftsState?.attributeCount[.model(file.fileId.id)] { - count = " \(presentationStringsFormattedNumber(counter, presentationData.dateTimeFormat.groupingSeparator))" - entities.append( - MessageTextEntity(range: title.count ..< title.count + count.count, type: .Bold) - ) - title += count - } - items.append(.action(ContextMenuActionItem(text: title, entities: entities, entityFiles: entityFiles, enableEntityAnimations: false, parseMarkdown: true, icon: { theme in - return isSelected ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil - }, action: { [weak self] _, f in - f(.default) - - if let self { - var updatedFilterAttributes = currentFilterAttributes - if selectedIds.contains(file.fileId.id) { - updatedFilterAttributes.removeAll(where: { $0 == .model(file.fileId.id) }) - } else { - updatedFilterAttributes.append(.model(file.fileId.id)) - } - self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) - } - }, longPressAction: { [weak self] _, f in - f(.default) - - if let self { - var updatedFilterAttributes = currentFilterAttributes.filter { attribute in - if case .model = attribute { - return false - } - return true - } - updatedFilterAttributes.append(.model(file.fileId.id)) - self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) - } - }))) - } - } - } - - let contextController = ContextController(context: component.context, presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + let contextController = ContextController( + context: component.context, + presentationData: presentationData, + source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), + items: .single(ContextController.Items(content: .list(items))), + gesture: nil + ) controller.presentInGlobalOverlay(contextController) } @@ -539,28 +424,70 @@ final class GiftStoreScreenComponent: Component { } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let searchQueryPromise = ValuePromise("") - var items: [ContextMenuItem] = [] - var allSelected = true - - var currentFilterAttributes: [ResaleGiftsContext.Attribute] = [] - var selectedIds = Set() - - if let filterAttributes = self.state?.starGiftsState?.filterAttributes { - currentFilterAttributes = filterAttributes - for attribute in filterAttributes { - if case let .backdrop(id) = attribute { - allSelected = false - selectedIds.insert(id) - } + let attributes = self.state?.starGiftsState?.attributes ?? [] + let backdropAttributes = attributes.filter { attribute in + if case .backdrop = attribute { + return true + } else { + return false } } - items.append(.action(ContextMenuActionItem(text: "Select All", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) - }, iconPosition: .left, action: { [weak self] _, f in - f(.default) - - if let self { + + let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? [] + let selectedBackdropAttributes = currentFilterAttributes.filter { attribute in + if case .backdrop = attribute { + return true + } else { + return false + } + } + + //TODO:localize + var items: [ContextMenuItem] = [] + items.append(.custom(SearchContextItem( + context: component.context, + placeholder: "Search", + value: "", + valueChanged: { value in + searchQueryPromise.set(value) + } + ), false)) + items.append(.separator) + items.append(.custom(GiftAttributeListContextItem( + context: component.context, + attributes: backdropAttributes, + selectedAttributes: selectedBackdropAttributes, + attributeCount: self.state?.starGiftsState?.attributeCount ?? [:], + searchQuery: searchQueryPromise.get(), + attributeSelected: { [weak self] attribute, exclusive in + guard let self else { + return + } + var updatedFilterAttributes: [ResaleGiftsContext.Attribute] + if exclusive { + updatedFilterAttributes = currentFilterAttributes.filter { attribute in + if case .backdrop = attribute { + return false + } + return true + } + updatedFilterAttributes.append(attribute) + } else { + updatedFilterAttributes = currentFilterAttributes + if selectedBackdropAttributes.contains(attribute) { + updatedFilterAttributes.removeAll(where: { $0 == attribute }) + } else { + updatedFilterAttributes.append(attribute) + } + } + self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) + }, + selectAll: { [weak self] in + guard let self else { + return + } let updatedFilterAttributes = currentFilterAttributes.filter { attribute in if case .backdrop = attribute { return false @@ -569,58 +496,15 @@ final class GiftStoreScreenComponent: Component { } self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) } - }))) + ), false)) - if let attributes = self.state?.starGiftsState?.attributes { - for attribute in attributes { - if case let .backdrop(name, id, innerColor, outerColor, _, _, _) = attribute { - let isSelected = allSelected || selectedIds.contains(id) - - var entities: [MessageTextEntity] = [] - var title = "\(name)" - var count = "" - if let counter = self.state?.starGiftsState?.attributeCount[.backdrop(id)] { - count = " \(presentationStringsFormattedNumber(counter, presentationData.dateTimeFormat.groupingSeparator))" - entities.append( - MessageTextEntity(range: title.count ..< title.count + count.count, type: .Bold) - ) - title += count - } - items.append(.action(ContextMenuActionItem(text: "\(name)\(count)", entities: entities, icon: { theme in - return isSelected ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil - }, additionalLeftIcon: { _ in - return generateGradientFilledCircleImage(diameter: 24.0, colors: [UIColor(rgb: UInt32(bitPattern: innerColor)).cgColor, UIColor(rgb: UInt32(bitPattern: outerColor)).cgColor]) - }, action: { [weak self] _, f in - f(.default) - - if let self { - var updatedFilterAttributes = currentFilterAttributes - if selectedIds.contains(id) { - updatedFilterAttributes.removeAll(where: { $0 == .backdrop(id) }) - } else { - updatedFilterAttributes.append(.backdrop(id)) - } - self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) - } - }, longPressAction: { [weak self] _, f in - f(.default) - - if let self { - var updatedFilterAttributes = currentFilterAttributes.filter { attribute in - if case .backdrop = attribute { - return false - } - return true - } - updatedFilterAttributes.append(.backdrop(id)) - self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) - } - }))) - } - } - } - - let contextController = ContextController(context: component.context, presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + let contextController = ContextController( + context: component.context, + presentationData: presentationData, + source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), + items: .single(ContextController.Items(content: .list(items))), + gesture: nil + ) controller.presentInGlobalOverlay(contextController) } @@ -630,28 +514,70 @@ final class GiftStoreScreenComponent: Component { } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let searchQueryPromise = ValuePromise("") - var items: [ContextMenuItem] = [] - var allSelected = true - - var currentFilterAttributes: [ResaleGiftsContext.Attribute] = [] - var selectedIds = Set() - - if let filterAttributes = self.state?.starGiftsState?.filterAttributes { - currentFilterAttributes = filterAttributes - for attribute in filterAttributes { - if case let .pattern(id) = attribute { - allSelected = false - selectedIds.insert(id) - } + let attributes = self.state?.starGiftsState?.attributes ?? [] + let patternAttributes = attributes.filter { attribute in + if case .pattern = attribute { + return true + } else { + return false } } - items.append(.action(ContextMenuActionItem(text: "Select All", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) - }, iconPosition: .left, action: { [weak self] _, f in - f(.default) - - if let self { + + let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? [] + let selectedPatternAttributes = currentFilterAttributes.filter { attribute in + if case .pattern = attribute { + return true + } else { + return false + } + } + + //TODO:localize + var items: [ContextMenuItem] = [] + items.append(.custom(SearchContextItem( + context: component.context, + placeholder: "Search", + value: "", + valueChanged: { value in + searchQueryPromise.set(value) + } + ), false)) + items.append(.separator) + items.append(.custom(GiftAttributeListContextItem( + context: component.context, + attributes: patternAttributes, + selectedAttributes: selectedPatternAttributes, + attributeCount: self.state?.starGiftsState?.attributeCount ?? [:], + searchQuery: searchQueryPromise.get(), + attributeSelected: { [weak self] attribute, exclusive in + guard let self else { + return + } + var updatedFilterAttributes: [ResaleGiftsContext.Attribute] + if exclusive { + updatedFilterAttributes = currentFilterAttributes.filter { attribute in + if case .pattern = attribute { + return false + } + return true + } + updatedFilterAttributes.append(attribute) + } else { + updatedFilterAttributes = currentFilterAttributes + if selectedPatternAttributes.contains(attribute) { + updatedFilterAttributes.removeAll(where: { $0 == attribute }) + } else { + updatedFilterAttributes.append(attribute) + } + } + self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) + }, + selectAll: { [weak self] in + guard let self else { + return + } let updatedFilterAttributes = currentFilterAttributes.filter { attribute in if case .pattern = attribute { return false @@ -660,65 +586,15 @@ final class GiftStoreScreenComponent: Component { } self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) } - }))) + ), false)) - if let attributes = self.state?.starGiftsState?.attributes { - for attribute in attributes { - if case let .pattern(name, file, _) = attribute { - let isSelected = allSelected || selectedIds.contains(file.fileId.id) - - var entities: [MessageTextEntity] = [] - var entityFiles: [Int64: TelegramMediaFile] = [:] - entities = [ - MessageTextEntity( - range: 0..<1, - type: .CustomEmoji(stickerPack: nil, fileId: file.fileId.id) - ) - ] - entityFiles[file.fileId.id] = file - - var title = "# \(name)" - var count = "" - if let counter = self.state?.starGiftsState?.attributeCount[.pattern(file.fileId.id)] { - count = " \(presentationStringsFormattedNumber(counter, presentationData.dateTimeFormat.groupingSeparator))" - entities.append( - MessageTextEntity(range: title.count ..< title.count + count.count, type: .Bold) - ) - title += count - } - items.append(.action(ContextMenuActionItem(text: title, entities: entities, entityFiles: entityFiles, enableEntityAnimations: false, icon: { theme in - return isSelected ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil - }, action: { [weak self] _, f in - f(.default) - - if let self { - var updatedFilterAttributes = currentFilterAttributes - if selectedIds.contains(file.fileId.id) { - updatedFilterAttributes.removeAll(where: { $0 == .pattern(file.fileId.id) }) - } else { - updatedFilterAttributes.append(.pattern(file.fileId.id)) - } - self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) - } - }, longPressAction: { [weak self] _, f in - f(.default) - - if let self { - var updatedFilterAttributes = currentFilterAttributes.filter { attribute in - if case .pattern = attribute { - return false - } - return true - } - updatedFilterAttributes.append(.pattern(file.fileId.id)) - self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) - } - }))) - } - } - } - - let contextController = ContextController(context: component.context, presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + let contextController = ContextController( + context: component.context, + presentationData: presentationData, + source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), + items: .single(ContextController.Items(content: .list(items))), + gesture: nil + ) controller.presentInGlobalOverlay(contextController) } @@ -729,7 +605,6 @@ final class GiftStoreScreenComponent: Component { } let environment = environment[EnvironmentType.self].value - let controller = environment.controller let themeUpdated = self.environment?.theme !== environment.theme self.environment = environment self.state = state @@ -785,6 +660,9 @@ final class GiftStoreScreenComponent: Component { let topSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelSize.height), size: CGSize(width: topSeparatorSize.width, height: topSeparatorSize.height)) if let topPanelView = self.topPanel.view, let topSeparatorView = self.topSeparator.view { if topPanelView.superview == nil { + topPanelView.alpha = 0.0 + topSeparatorView.alpha = 0.0 + self.addSubview(topPanelView) self.addSubview(topSeparatorView) } @@ -792,51 +670,19 @@ final class GiftStoreScreenComponent: Component { transition.setFrame(view: topSeparatorView, frame: topSeparatorFrame) } - let cancelButtonSize = self.cancelButton.update( - transition: transition, - component: AnyComponent( - PlainButtonComponent( - content: AnyComponent( - MultilineTextComponent( - text: .plain(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor)), - horizontalAlignment: .center - ) - ), - effectAlignment: .center, - action: { - controller()?.dismiss() - }, - animateScale: false - ) - ), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 100.0) - ) - let cancelButtonFrame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 16.0, y: environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0 - cancelButtonSize.height / 2.0), size: cancelButtonSize) - if let cancelButtonView = self.cancelButton.view { - if cancelButtonView.superview == nil { - self.addSubview(cancelButtonView) - } - transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame) - } - -// let showFilters = !"".isEmpty - -// let sortButtonSize = self.sortButton.update( +// let cancelButtonSize = self.cancelButton.update( // transition: transition, // component: AnyComponent( // PlainButtonComponent( // content: AnyComponent( -// BundleIconComponent( -// name: "Peer Info/SortIcon", -// tintColor: theme.rootController.navigationBar.accentTextColor +// MultilineTextComponent( +// text: .plain(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor)), +// horizontalAlignment: .center // ) // ), // effectAlignment: .center, -// action: { [weak self] in -// if let sourceView = self?.sortButton.view { -// self?.openContextMenu(sourceView: sourceView) -// } +// action: { +// controller()?.dismiss() // }, // animateScale: false // ) @@ -844,15 +690,14 @@ final class GiftStoreScreenComponent: Component { // environment: {}, // containerSize: CGSize(width: availableSize.width, height: 100.0) // ) -// let sortButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - environment.safeInsets.right - sortButtonSize.width - 10.0, y: environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0 - sortButtonSize.height / 2.0), size: sortButtonSize) -// if let sortButtonView = self.sortButton.view { -// if sortButtonView.superview == nil { -// self.addSubview(sortButtonView) +// let cancelButtonFrame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 16.0, y: environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0 - cancelButtonSize.height / 2.0), size: cancelButtonSize) +// if let cancelButtonView = self.cancelButton.view { +// if cancelButtonView.superview == nil { +// self.addSubview(cancelButtonView) // } -// transition.setFrame(view: sortButtonView, frame: sortButtonFrame) -// transition.setAlpha(view: sortButtonView, alpha: showFilters ? 0.0 : 1.0) +// transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame) // } - + let balanceTitleSize = self.balanceTitle.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( @@ -902,16 +747,12 @@ final class GiftStoreScreenComponent: Component { balanceValueView.bounds = CGRect(origin: .zero, size: balanceValueSize) balanceIconView.center = CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - balanceValueSize.width - balanceIconSize.width / 2.0 - 2.0, y: topBalanceOriginY + balanceTitleSize.height + balanceValueSize.height / 2.0 - UIScreenPixel) balanceIconView.bounds = CGRect(origin: .zero, size: balanceIconSize) - -// transition.setAlpha(view: balanceTitleView, alpha: showFilters ? 1.0 : 0.0) -// transition.setAlpha(view: balanceValueView, alpha: showFilters ? 1.0 : 0.0) -// transition.setAlpha(view: balanceIconView, alpha: showFilters ? 1.0 : 0.0) } let titleSize = self.title.update( transition: transition, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "Gift Name", font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)), + text: .plain(NSAttributedString(string: component.gift.title ?? "Gift", font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)), horizontalAlignment: .center )), environment: {}, @@ -924,10 +765,19 @@ final class GiftStoreScreenComponent: Component { transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: 10.0), size: titleSize)) } + let effectiveCount: Int32 + if let count = self.effectiveGifts?.count { + effectiveCount = Int32(count) + } else if let resale = component.gift.availability?.resale { + effectiveCount = Int32(resale) + } else { + effectiveCount = 0 + } + let subtitleSize = self.subtitle.update( transition: transition, component: AnyComponent(BalancedTextComponent( - text: .plain(NSAttributedString(string: "\(self.effectiveGifts?.count ?? 0) for resale", font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)), + text: .plain(NSAttributedString(string: "\(effectiveCount) for resale", font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 1 )), @@ -946,20 +796,25 @@ final class GiftStoreScreenComponent: Component { let optionWidth = (availableSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0 var sortingTitle = "Date" + var sortingIcon: String = "Peer Info/SortDate" if let sorting = self.state?.starGiftsState?.sorting { switch sorting { case .date: sortingTitle = "Date" + sortingIcon = "Peer Info/SortDate" case .value: sortingTitle = "Price" + sortingIcon = "Peer Info/SortValue" case .number: sortingTitle = "Number" + sortingIcon = "Peer Info/SortNumber" } } var filterItems: [FilterSelectorComponent.Item] = [] filterItems.append(FilterSelectorComponent.Item( id: AnyHashable(0), + iconName: sortingIcon, title: sortingTitle, action: { [weak self] view in if let self { @@ -1043,7 +898,7 @@ final class GiftStoreScreenComponent: Component { component: AnyComponent(FilterSelectorComponent( context: component.context, colors: FilterSelectorComponent.Colors( - foreground: theme.list.itemSecondaryTextColor, + foreground: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.65), background: theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15) ), items: filterItems @@ -1081,7 +936,7 @@ final class GiftStoreScreenComponent: Component { self.scrollView.contentSize = contentSize self.nextScrollTransition = nil } - let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0) + let scrollInsets = UIEdgeInsets(top: topPanelHeight, left: 0.0, bottom: 0.0, right: 0.0) if self.scrollView.scrollIndicatorInsets != scrollInsets { self.scrollView.scrollIndicatorInsets = scrollInsets } @@ -1113,7 +968,7 @@ final class GiftStoreScreenComponent: Component { transition.setFrame(view: self.loadingNode.view, frame: CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight + 39.0 + 7.0), size: availableSize)) let fadeTransition = ComponentTransition.easeInOut(duration: 0.25) - if let effectiveGifts = self.effectiveGifts, effectiveGifts.isEmpty && !self.isLoading { + if let effectiveGifts = self.effectiveGifts, effectiveGifts.isEmpty && self.state?.starGiftsState?.dataState != .loading { let sideInset: CGFloat = 44.0 let emptyAnimationHeight = 148.0 let topInset: CGFloat = environment.navigationHeight + 39.0 @@ -1149,10 +1004,7 @@ final class GiftStoreScreenComponent: Component { guard let self else { return } - self.selectedModels.removeAll() - self.selectedBackdrops.removeAll() - self.selectedSymbols.removeAll() - self.simulateLoading() + self.state?.starGiftsContext.updateFilterAttributes([]) }, animateScale: false ) @@ -1205,6 +1057,8 @@ final class GiftStoreScreenComponent: Component { } view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size) ComponentTransition.immediate.setPosition(view: view, position: emptyResultsActionFrame.center) + + view.alpha = self.state?.starGiftsState?.attributes.isEmpty == true ? 0.0 : 1.0 } } else { if let view = self.emptyResultsAnimation.view { @@ -1234,6 +1088,7 @@ final class GiftStoreScreenComponent: Component { final class State: ComponentState { private let context: AccountContext + var peerId: EnginePeer.Id private var disposable: Disposable? fileprivate let starGiftsContext: ResaleGiftsContext @@ -1241,9 +1096,11 @@ final class GiftStoreScreenComponent: Component { init( context: AccountContext, + peerId: EnginePeer.Id, giftId: Int64 ) { self.context = context + self.peerId = peerId self.starGiftsContext = ResaleGiftsContext(account: context.account, giftId: giftId) super.init() @@ -1264,7 +1121,7 @@ final class GiftStoreScreenComponent: Component { } func makeState() -> State { - return State(context: self.context, giftId: self.gift.id) + return State(context: self.context, peerId: self.peerId, giftId: self.gift.id) } func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { @@ -1292,11 +1149,10 @@ public class GiftStoreScreen: ViewControllerComponentContainer { starsContext: starsContext, peerId: peerId, gift: gift - ), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil) + ), navigationBarAppearance: .transparent, theme: .default, updatedPresentationData: nil) self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.context.sharedContext.currentPresentationData.with { $0 }.strings.Common_Back, style: .plain, target: nil, action: nil) - self.scrollToTop = { [weak self] in guard let self, let componentView = self.node.hostView.componentView as? GiftStoreScreenComponent.View else { return @@ -1332,6 +1188,8 @@ private final class GiftStoreReferenceContentSource: ContextReferenceContentSour private let controller: ViewController private let sourceView: UIView + let forceDisplayBelowKeyboard = true + init(controller: ViewController, sourceView: UIView) { self.controller = controller self.sourceView = sourceView diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/LoadingShimmerComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/LoadingShimmerComponent.swift index ed76cec440..b9f6c04b2f 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/LoadingShimmerComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/LoadingShimmerComponent.swift @@ -134,6 +134,7 @@ final class LoadingShimmerNode: ASDisplayNode { super.init() + self.allowsGroupOpacity = true self.isUserInteractionEnabled = false self.addSubnode(self.backgroundColorNode) diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/SearchContextItem.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/SearchContextItem.swift new file mode 100644 index 0000000000..585a863f69 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/SearchContextItem.swift @@ -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() + private let inputField = ComponentView() + private let inputFieldExternalState = TextFieldComponent.ExternalState() + private let inputPlaceholderView = ComponentView() + private let inputClear = ComponentView() + 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 + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift index de4bce26f3..12336824b9 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift @@ -9,6 +9,95 @@ import MoreButtonNode import AccountContext import TelegramPresentationData +final class PriceButtonComponent: Component { + let price: Int64 + + init( + price: Int64 + ) { + self.price = price + } + + static func ==(lhs: PriceButtonComponent, rhs: PriceButtonComponent) -> Bool { + return lhs.price == rhs.price + } + + final class View: UIView { + private let backgroundView = UIView() + + private let icon = UIImageView() + private let text = ComponentView() + + 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, 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, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + final class ButtonsComponent: Component { let theme: PresentationTheme let isOverlay: Bool diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index bb22cbe632..3125cacc51 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -53,11 +53,13 @@ private final class GiftViewSheetContent: CombinedComponent { let convertToStars: () -> Void let openStarsIntro: () -> Void let sendGift: (EnginePeer.Id) -> Void + let changeRecipient: () -> Void let openMyGifts: () -> Void let transferGift: () -> Void let upgradeGift: ((Int64?, Bool) -> Signal) + let buyGift: ((String, EnginePeer.Id) -> Signal) let shareGift: () -> Void - let resellGift: () -> Void + let resellGift: (Bool) -> Void let showAttributeInfo: (Any, String) -> Void let viewUpgraded: (EngineMessage.Id) -> Void let openMore: (ASDisplayNode, ContextGesture?) -> Void @@ -74,11 +76,13 @@ private final class GiftViewSheetContent: CombinedComponent { convertToStars: @escaping () -> Void, openStarsIntro: @escaping () -> Void, sendGift: @escaping (EnginePeer.Id) -> Void, + changeRecipient: @escaping () -> Void, openMyGifts: @escaping () -> Void, transferGift: @escaping () -> Void, upgradeGift: @escaping ((Int64?, Bool) -> Signal), + buyGift: @escaping ((String, EnginePeer.Id) -> Signal), shareGift: @escaping () -> Void, - resellGift: @escaping () -> Void, + resellGift: @escaping (Bool) -> Void, showAttributeInfo: @escaping (Any, String) -> Void, viewUpgraded: @escaping (EngineMessage.Id) -> Void, openMore: @escaping (ASDisplayNode, ContextGesture?) -> Void, @@ -94,9 +98,11 @@ private final class GiftViewSheetContent: CombinedComponent { self.convertToStars = convertToStars self.openStarsIntro = openStarsIntro self.sendGift = sendGift + self.changeRecipient = changeRecipient self.openMyGifts = openMyGifts self.transferGift = transferGift self.upgradeGift = upgradeGift + self.buyGift = buyGift self.shareGift = shareGift self.resellGift = resellGift self.showAttributeInfo = showAttributeInfo @@ -118,12 +124,22 @@ private final class GiftViewSheetContent: CombinedComponent { final class State: ComponentState { private let context: AccountContext private(set) var subject: GiftViewScreen.Subject + private let upgradeGift: ((Int64?, Bool) -> Signal) + private let buyGift: ((String, EnginePeer.Id) -> Signal) + private let getController: () -> ViewController? private var disposable: Disposable? var initialized = false + var recipientPeerIdPromise = ValuePromise(nil) + var recipientPeerId: EnginePeer.Id? { + didSet { + self.recipientPeerIdPromise.set(self.recipientPeerId) + } + } + var peerMap: [EnginePeer.Id: EnginePeer] = [:] var starGiftsMap: [Int64: StarGift.Gift] = [:] @@ -161,11 +177,13 @@ private final class GiftViewSheetContent: CombinedComponent { context: AccountContext, subject: GiftViewScreen.Subject, upgradeGift: @escaping ((Int64?, Bool) -> Signal), + buyGift: @escaping ((String, EnginePeer.Id) -> Signal), getController: @escaping () -> ViewController? ) { self.context = context self.subject = subject self.upgradeGift = upgradeGift + self.buyGift = buyGift self.getController = getController super.init() @@ -187,6 +205,7 @@ private final class GiftViewSheetContent: CombinedComponent { peerIds.append(contentsOf: media.peerIds) } } + if case let .unique(gift) = arguments.gift { if case let .peerId(peerId) = gift.owner { peerIds.append(peerId) @@ -247,18 +266,38 @@ private final class GiftViewSheetContent: CombinedComponent { } } - self.disposable = combineLatest(queue: Queue.mainQueue(), - context.engine.data.get(EngineDataMap( - peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in - return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + let peerIdsSignal: Signal<[EnginePeer.Id], NoError> + if case let .uniqueGift(_, recipientPeerIdValue) = subject, let recipientPeerIdValue { + self.recipientPeerId = recipientPeerIdValue + self.recipientPeerIdPromise.set(recipientPeerIdValue) + peerIdsSignal = self.recipientPeerIdPromise.get() + |> map { recipientPeerId in + var peerIds = peerIds + if let recipientPeerId { + peerIds.append(recipientPeerId) } - )), + return peerIds + } + } else { + peerIdsSignal = .single(peerIds) + } + + self.disposable = combineLatest(queue: Queue.mainQueue(), + peerIdsSignal + |> distinctUntilChanged + |> mapToSignal { peerIds in + return context.engine.data.get(EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in + return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + } + )) + }, .single(nil) |> then(context.engine.payments.cachedStarGifts()) ).startStrict(next: { [weak self] peers, starGifts in if let strongSelf = self { var peersMap: [EnginePeer.Id: EnginePeer] = [:] - for peerId in peerIds { - if let maybePeer = peers[peerId], let peer = maybePeer { + for (peerId, maybePeer) in peers { + if let peer = maybePeer { peersMap[peerId] = peer } } @@ -281,7 +320,12 @@ private final class GiftViewSheetContent: CombinedComponent { }) } - if let starsContext = context.starsContext, let state = starsContext.currentState, state.balance < StarsAmount(value: 100, nanos: 0) { + var minRequiredAmount = StarsAmount(value: 100, nanos: 0) + if let resellStars = self.subject.arguments?.resellStars { + minRequiredAmount = StarsAmount(value: resellStars, nanos: 0) + } + + if let starsContext = context.starsContext, let state = starsContext.currentState, state.balance < minRequiredAmount { self.optionsPromise.set(context.engine.payments.starsTopUpOptions() |> map(Optional.init)) } @@ -289,7 +333,7 @@ private final class GiftViewSheetContent: CombinedComponent { if let controller = getController() as? GiftViewScreen { controller.updateSubject.connect { [weak self] subject in self?.subject = subject - self?.updated() + self?.updated(transition: .easeInOut(duration: 0.25)) } } } @@ -359,35 +403,124 @@ private final class GiftViewSheetContent: CombinedComponent { } } + func changeRecipient() { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let mode = ContactSelectionControllerMode.starsGifting(birthdays: nil, hasActions: false, showSelf: true, selfSubtitle: presentationData.strings.Premium_Gift_ContactSelection_BuySelf) + + //TODO:localize + let controller = self.context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams( + context: context, + mode: mode, + autoDismiss: true, + title: { _ in return "Change Recipient" }, + options: .single([]), + allowChannelsInSearch: false + )) + controller.navigationPresentation = .modal + let _ = (controller.result + |> deliverOnMainQueue).start(next: { [weak self] result in + if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer { + self?.recipientPeerId = peer.id + } + }) + + self.getController()?.push(controller) + } + func commitBuy() { - guard let arguments = self.subject.arguments, let _ = arguments.peerId, let starsContext = self.context.starsContext, let starsState = starsContext.currentState, case let .unique(uniqueGift) = arguments.gift else { + guard let resellStars = self.subject.arguments?.resellStars, let starsContext = self.context.starsContext, let starsState = starsContext.currentState, case let .unique(uniqueGift) = self.subject.arguments?.gift else { return } + + let giftTitle = "\(uniqueGift.title) #\(uniqueGift.number)" + + let context = self.context + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let recipientPeerId = self.recipientPeerId ?? self.context.account.peerId let action = { let proceed: (Int64) -> Void = { formId in self.inProgress = true self.updated() - let signal = self.context.engine.payments.sendStarsPaymentForm(formId: formId, source: .starGiftResale(slug: uniqueGift.slug, toPeerId: self.context.account.peerId)) - |> mapError { _ -> SendBotPaymentFormError in - return .generic - } - |> mapToSignal { result in - if case let .done(_, _, gift) = result, let gift { - return .single(gift) - } else { - return .complete() - } - } - - self.buyDisposable = (signal - |> deliverOnMainQueue).start(next: { [weak self, weak starsContext] result in + self.buyDisposable = (self.buyGift(uniqueGift.slug, recipientPeerId) + |> deliverOnMainQueue).start(completed: { [weak self, weak starsContext] in guard let self, let controller = self.getController() as? GiftViewScreen else { return } self.inProgress = false + var animationFile: TelegramMediaFile? + for attribute in uniqueGift.attributes { + if case let .model(_, file, _) = attribute { + animationFile = file + break + } + } + + if let navigationController = controller.navigationController as? NavigationController { + if recipientPeerId == self.context.account.peerId { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak navigationController] peer in + guard let peer, let navigationController else { + return + } + + var controllers = Array(navigationController.viewControllers.prefix(1)) + if let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .myProfileGifts, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + controllers.append(controller) + } + navigationController.setViewControllers(controllers, animated: true) + navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds)) + + Queue.mainQueue().after(0.5, { + if let lastController = navigationController.viewControllers.last as? ViewController, let animationFile { + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .sticker(context: context, file: animationFile, loop: false, title: "Gift Acquired", text: "\(giftTitle) is now yours.", undoText: nil, customAction: nil), + elevatedLayout: lastController is ChatController, + action: { _ in + return true + } + ) + lastController.present(resultController, in: .window(.root)) + } + }) + }) + } else { + var controllers = Array(navigationController.viewControllers.prefix(1)) + let chatController = self.context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: recipientPeerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) + chatController.hintPlayNextOutgoingGift() + controllers.append(chatController) + navigationController.setViewControllers(controllers, animated: true) + + Queue.mainQueue().after(0.5, { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: recipientPeerId)) + |> deliverOnMainQueue).start(next: { [weak navigationController] peer in + if let peer, let lastController = navigationController?.viewControllers.last as? ViewController, let animationFile { + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .sticker(context: context, file: animationFile, loop: false, title: "Gift Sent", text: "\(peer.compactDisplayTitle) has been notified about your gift.", undoText: nil, customAction: nil), + elevatedLayout: lastController is ChatController, + action: { _ in + return true + } + ) + lastController.present(resultController, in: .window(.root)) + } + }) + }) + } + } + controller.animateSuccess() self.updated(transition: .spring(duration: 0.4)) @@ -420,7 +553,7 @@ private final class GiftViewSheetContent: CombinedComponent { starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) let _ = (starsContext.onUpdate - |> deliverOnMainQueue).start(next: { + |> deliverOnMainQueue).start(next: { proceed(buyForm.id) }) } @@ -432,24 +565,37 @@ private final class GiftViewSheetContent: CombinedComponent { } } } - let giftTitle = "\(uniqueGift.title) #\(uniqueGift.number)" - let alertController = textAlertController( - context: self.context, - title: "Confirm Payment", - text: "Do you really want to buy **\(giftTitle)** for **\(arguments.resellStars ?? 0)** Stars?", - actions: [ - TextAlertAction(type: .defaultAction, title: "Buy for \(arguments.resellStars ?? 0) Stars", action: { - action() - }), - TextAlertAction(type: .genericAction, title: "Cancel", action: { - }) - ], - actionLayout: .vertical, - parseMarkdown: true - ) - if let controller = self.getController() as? GiftViewScreen { - controller.present(alertController, in: .window(.root)) - } + + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: recipientPeerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + let text: String + if recipientPeerId == self.context.account.peerId { + text = "Do you really want to buy **\(giftTitle)** for **\(resellStars)** Stars?" + } else { + text = "Do you really want to buy **\(giftTitle)** for **\(resellStars)** Stars and gift it to **\(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))**?" + } + + let alertController = textAlertController( + context: self.context, + title: "Confirm Payment", + text: text, + actions: [ + TextAlertAction(type: .defaultAction, title: "Buy for \(resellStars) Stars", action: { + action() + }), + TextAlertAction(type: .genericAction, title: "Cancel", action: { + }) + ], + actionLayout: .vertical, + parseMarkdown: true + ) + if let controller = self.getController() as? GiftViewScreen { + controller.present(alertController, in: .window(.root)) + } + }) } func commitUpgrade() { @@ -527,10 +673,12 @@ private final class GiftViewSheetContent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, subject: self.subject, upgradeGift: self.upgradeGift, getController: self.getController) + return State(context: self.context, subject: self.subject, upgradeGift: self.upgradeGift, buyGift: self.buyGift, getController: self.getController) } static var body: Body { + let priceButton = Child(PlainButtonComponent.self) + let buttons = Child(ButtonsComponent.self) let animation = Child(GiftCompositionComponent.self) let title = Child(MultilineTextComponent.self) @@ -538,7 +686,6 @@ private final class GiftViewSheetContent: CombinedComponent { let transferButton = Child(PlainButtonComponent.self) let wearButton = Child(PlainButtonComponent.self) -// let shareButton = Child(PlainButtonComponent.self) let resellButton = Child(PlainButtonComponent.self) let wearAvatar = Child(AvatarComponent.self) @@ -599,6 +746,7 @@ private final class GiftViewSheetContent: CombinedComponent { var uniqueGift: StarGift.UniqueGift? var isSelfGift = false var isChannelGift = false + var isMyUniqueGift = false if case let .soldOutGift(gift) = subject { animationFile = gift.file @@ -646,6 +794,10 @@ private final class GiftViewSheetContent: CombinedComponent { isSelfGift = arguments.messageId?.peerId == component.context.account.peerId + if case let .peerId(peerId) = uniqueGift?.owner, peerId == component.context.account.peerId || isChannelGift { + isMyUniqueGift = true + } + if isSelfGift { titleString = strings.Gift_View_Self_Title } else { @@ -1148,12 +1300,7 @@ private final class GiftViewSheetContent: CombinedComponent { var descriptionText: String if let uniqueGift { titleString = uniqueGift.title - - if let resellPrice = uniqueGift.resellStars, incoming { - descriptionText = "\(strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator)) • Listed for * \(resellPrice)" - } else { - descriptionText = "\(strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))" - } + descriptionText = "\(strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))" } else if soldOut { descriptionText = strings.Gift_View_UnavailableDescription } else if upgraded { @@ -1341,142 +1488,180 @@ private final class GiftViewSheetContent: CombinedComponent { if !soldOut { if let uniqueGift { - switch uniqueGift.owner { - case let .peerId(peerId): - if let peer = state.peerMap[peerId] { - let ownerComponent: AnyComponent - if peer.id == component.context.account.peerId, peer.isPremium { - let animationContent: EmojiStatusComponent.Content - var color: UIColor? - var statusId: Int64 = 1 - if state.pendingWear { - var fileId: Int64? - for attribute in uniqueGift.attributes { - if case let .model(_, file, _) = attribute { - fileId = file.fileId.id - } - if case let .backdrop(_, _, innerColor, _, _, _, _) = attribute { - color = UIColor(rgb: UInt32(bitPattern: innerColor)) - } - } - if let fileId { - statusId = fileId - animationContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 18.0, height: 18.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: tableLinkColor, loopMode: .count(2)) - } else { - animationContent = .premium(color: tableLinkColor) - } - } else if let emojiStatus = peer.emojiStatus, !state.pendingTakeOff { - animationContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 18.0, height: 18.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: tableLinkColor, loopMode: .count(2)) - if case let .starGift(id, _, _, _, _, innerColor, _, _, _) = emojiStatus.content { - color = UIColor(rgb: UInt32(bitPattern: innerColor)) - if id == uniqueGift.id { - isWearing = true - state.pendingWear = false - } - } - } else { - animationContent = .premium(color: tableLinkColor) - state.pendingTakeOff = false - } - - ownerComponent = AnyComponent( - HStack([ - AnyComponentWithIdentity( - id: AnyHashable(0), - component: AnyComponent(Button( - content: AnyComponent( - PeerCellComponent( + if case let .uniqueGift(_, recipientPeerIdValue) = component.subject, let _ = recipientPeerIdValue, let recipientPeerId = state.recipientPeerId { + //TODO:localize + if let peer = state.peerMap[recipientPeerId] { + tableItems.append(.init( + id: "recipient", + title: "Recipient", + component: AnyComponent( + Button( + content: AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: AnyHashable(peer.id), + component: AnyComponent(PeerCellComponent( context: component.context, theme: theme, strings: strings, peer: peer - ) + )) ), - action: { - component.openPeer(peer) - Queue.mainQueue().after(0.6, { - component.cancel(false) - }) - } - )) + AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(ButtonContentComponent( + context: component.context, + text: "change", + color: theme.list.itemAccentColor + )) + ) + ], spacing: 4.0) ), - AnyComponentWithIdentity( - id: AnyHashable(statusId), - component: AnyComponent(EmojiStatusComponent( - context: component.context, - animationCache: component.context.animationCache, - animationRenderer: component.context.animationRenderer, - content: animationContent, - particleColor: color, - size: CGSize(width: 18.0, height: 18.0), - isVisibleForAnimations: true, - action: { - - }, - tag: statusTag - )) - ) - ], spacing: 2.0) + action: { [weak state] in + state?.changeRecipient() + } + ) ) - } else { - ownerComponent = AnyComponent(Button( - content: AnyComponent( - PeerCellComponent( - context: component.context, - theme: theme, - strings: strings, - peer: peer - ) - ), - action: { - component.openPeer(peer) - Queue.mainQueue().after(0.6, { - component.cancel(false) - }) - } - )) - } - tableItems.append(.init( - id: "owner", - title: strings.Gift_Unique_Owner, - component: ownerComponent )) } - case let .name(name): - tableItems.append(.init( - id: "name_owner", - title: strings.Gift_Unique_Owner, - component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: name, font: tableFont, textColor: tableTextColor))) - ) - )) - case let .address(address): - exported = true - - func formatAddress(_ str: String) -> String { - guard str.count == 48 && !str.hasSuffix(".ton") else { - return str - } - var result = str - let middleIndex = result.index(result.startIndex, offsetBy: str.count / 2) - result.insert("\n", at: middleIndex) - return result - } - - tableItems.append(.init( - id: "address_owner", - title: strings.Gift_Unique_Owner, - component: AnyComponent( - Button( - content: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: formatAddress(address), font: tableLargeMonospaceFont, textColor: tableLinkColor)), maximumNumberOfLines: 2, lineSpacing: 0.2) - ), - action: { - component.copyAddress(address) + } else { + switch uniqueGift.owner { + case let .peerId(peerId): + if let peer = state.peerMap[peerId] { + let ownerComponent: AnyComponent + if peer.id == component.context.account.peerId, peer.isPremium { + let animationContent: EmojiStatusComponent.Content + var color: UIColor? + var statusId: Int64 = 1 + if state.pendingWear { + var fileId: Int64? + for attribute in uniqueGift.attributes { + if case let .model(_, file, _) = attribute { + fileId = file.fileId.id + } + if case let .backdrop(_, _, innerColor, _, _, _, _) = attribute { + color = UIColor(rgb: UInt32(bitPattern: innerColor)) + } + } + if let fileId { + statusId = fileId + animationContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 18.0, height: 18.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: tableLinkColor, loopMode: .count(2)) + } else { + animationContent = .premium(color: tableLinkColor) + } + } else if let emojiStatus = peer.emojiStatus, !state.pendingTakeOff { + animationContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 18.0, height: 18.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: tableLinkColor, loopMode: .count(2)) + if case let .starGift(id, _, _, _, _, innerColor, _, _, _) = emojiStatus.content { + color = UIColor(rgb: UInt32(bitPattern: innerColor)) + if id == uniqueGift.id { + isWearing = true + state.pendingWear = false + } + } + } else { + animationContent = .premium(color: tableLinkColor) + state.pendingTakeOff = false } + + ownerComponent = AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(Button( + content: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + strings: strings, + peer: peer + ) + ), + action: { + component.openPeer(peer) + Queue.mainQueue().after(0.6, { + component.cancel(false) + }) + } + )) + ), + AnyComponentWithIdentity( + id: AnyHashable(statusId), + component: AnyComponent(EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: animationContent, + particleColor: color, + size: CGSize(width: 18.0, height: 18.0), + isVisibleForAnimations: true, + action: { + + }, + tag: statusTag + )) + ) + ], spacing: 2.0) + ) + } else { + ownerComponent = AnyComponent(Button( + content: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + strings: strings, + peer: peer + ) + ), + action: { + component.openPeer(peer) + Queue.mainQueue().after(0.6, { + component.cancel(false) + }) + } + )) + } + tableItems.append(.init( + id: "owner", + title: strings.Gift_Unique_Owner, + component: ownerComponent + )) + } + case let .name(name): + tableItems.append(.init( + id: "name_owner", + title: strings.Gift_Unique_Owner, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: name, font: tableFont, textColor: tableTextColor))) ) - ) - )) + )) + case let .address(address): + exported = true + + func formatAddress(_ str: String) -> String { + guard str.count == 48 && !str.hasSuffix(".ton") else { + return str + } + var result = str + let middleIndex = result.index(result.startIndex, offsetBy: str.count / 2) + result.insert("\n", at: middleIndex) + return result + } + + tableItems.append(.init( + id: "address_owner", + title: strings.Gift_Unique_Owner, + component: AnyComponent( + Button( + content: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: formatAddress(address), font: tableLargeMonospaceFont, textColor: tableLinkColor)), maximumNumberOfLines: 2, lineSpacing: 0.2) + ), + action: { + component.copyAddress(address) + } + ) + ) + )) + } } } else if let peerId = subject.arguments?.fromPeerId, let peer = state.peerMap[peerId] { var isBot = false @@ -1568,7 +1753,7 @@ private final class GiftViewSheetContent: CombinedComponent { } if let uniqueGift { - if case let .peerId(peerId) = uniqueGift.owner, peerId == component.context.account.peerId || isChannelGift { + if isMyUniqueGift, case let .peerId(peerId) = uniqueGift.owner { var canTransfer = true if let peer = state.peerMap[peerId], case let .channel(channel) = peer, !channel.flags.contains(.isCreator) { canTransfer = false @@ -1679,7 +1864,7 @@ private final class GiftViewSheetContent: CombinedComponent { ), effectAlignment: .center, action: { - component.resellGift() + component.resellGift(false) } ), environment: {}, @@ -1691,29 +1876,6 @@ private final class GiftViewSheetContent: CombinedComponent { .appear(.default(scale: true, alpha: true)) .disappear(.default(scale: true, alpha: true)) ) - -// let shareButton = shareButton.update( -// component: PlainButtonComponent( -// content: AnyComponent( -// HeaderButtonComponent( -// title: strings.Gift_View_Header_Share, -// iconName: "Premium/Collectible/Share" -// ) -// ), -// effectAlignment: .center, -// action: { -// component.shareGift() -// } -// ), -// environment: {}, -// availableSize: CGSize(width: buttonWidth, height: buttonHeight), -// transition: context.transition -// ) -// context.add(shareButton -// .position(CGPoint(x: buttonOriginX + buttonWidth / 2.0, y: headerHeight - buttonHeight / 2.0 - 16.0)) -// .appear(.default(scale: true, alpha: true)) -// .disappear(.default(scale: true, alpha: true)) -// ) } let showAttributeInfo = component.showAttributeInfo @@ -2064,19 +2226,50 @@ private final class GiftViewSheetContent: CombinedComponent { ) originY += table.size.height + 23.0 } - + var resellStars: Int64? + var selling = false if let uniqueGift { resellStars = uniqueGift.resellStars + + if incoming, let resellStars { + let priceButton = priceButton.update( + component: PlainButtonComponent( + content: AnyComponent( + PriceButtonComponent(price: resellStars) + ), + effectAlignment: .center, + action: { + component.resellGift(true) + }, + animateScale: false + ), + availableSize: CGSize(width: 120.0, height: 30.0), + transition: context.transition + ) + context.add(priceButton + .position(CGPoint(x: environment.safeInsets.left + 16.0 + priceButton.size.width / 2.0, y: 28.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + } + + if !incoming, let _ = resellStars { + if case let .uniqueGift(_, recipientPeerId) = component.subject, recipientPeerId != nil { + } else { + selling = true + } + } } - if ((incoming && !converted && !upgraded) || exported || (!incoming && resellStars != nil)) && (!showUpgradePreview && !showWearPreview) { + + if ((incoming && !converted && !upgraded) || exported || selling) && (!showUpgradePreview && !showWearPreview) { let linkColor = theme.actionSheet.controlAccentColor if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme { state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme) } var addressToOpen: String? var descriptionText: String - if let uniqueGift, !incoming { + if let uniqueGift, selling { //TODO:localize let ownerName: String if case let .peerId(peerId) = uniqueGift.owner { @@ -2377,10 +2570,11 @@ private final class GiftViewSheetContent: CombinedComponent { availableSize: buttonSize, transition: context.transition ) - } else if !incoming, let resellStars { + } else if !incoming, let resellStars, !isMyUniqueGift { if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme { state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme) } + //TODO:localize var upgradeString = "Buy for" upgradeString += " # \(presentationStringsFormattedNumber(Int32(resellStars), environment.dateTimeFormat.groupingSeparator))" @@ -2454,11 +2648,13 @@ private final class GiftViewSheetComponent: CombinedComponent { let convertToStars: () -> Void let openStarsIntro: () -> Void let sendGift: (EnginePeer.Id) -> Void + let changeRecipient: () -> Void let openMyGifts: () -> Void let transferGift: () -> Void let upgradeGift: ((Int64?, Bool) -> Signal) + let buyGift: ((String, EnginePeer.Id) -> Signal) let shareGift: () -> Void - let resellGift: () -> Void + let resellGift: (Bool) -> Void let viewUpgraded: (EngineMessage.Id) -> Void let openMore: (ASDisplayNode, ContextGesture?) -> Void let showAttributeInfo: (Any, String) -> Void @@ -2473,11 +2669,13 @@ private final class GiftViewSheetComponent: CombinedComponent { convertToStars: @escaping () -> Void, openStarsIntro: @escaping () -> Void, sendGift: @escaping (EnginePeer.Id) -> Void, + changeRecipient: @escaping () -> Void, openMyGifts: @escaping () -> Void, transferGift: @escaping () -> Void, upgradeGift: @escaping ((Int64?, Bool) -> Signal), + buyGift: @escaping ((String, EnginePeer.Id) -> Signal), shareGift: @escaping () -> Void, - resellGift: @escaping () -> Void, + resellGift: @escaping (Bool) -> Void, viewUpgraded: @escaping (EngineMessage.Id) -> Void, openMore: @escaping (ASDisplayNode, ContextGesture?) -> Void, showAttributeInfo: @escaping (Any, String) -> Void @@ -2491,9 +2689,11 @@ private final class GiftViewSheetComponent: CombinedComponent { self.convertToStars = convertToStars self.openStarsIntro = openStarsIntro self.sendGift = sendGift + self.changeRecipient = changeRecipient self.openMyGifts = openMyGifts self.transferGift = transferGift self.upgradeGift = upgradeGift + self.buyGift = buyGift self.shareGift = shareGift self.resellGift = resellGift self.viewUpgraded = viewUpgraded @@ -2542,9 +2742,11 @@ private final class GiftViewSheetComponent: CombinedComponent { convertToStars: context.component.convertToStars, openStarsIntro: context.component.openStarsIntro, sendGift: context.component.sendGift, + changeRecipient: context.component.changeRecipient, openMyGifts: context.component.openMyGifts, transferGift: context.component.transferGift, upgradeGift: context.component.upgradeGift, + buyGift: context.component.buyGift, shareGift: context.component.shareGift, resellGift: context.component.resellGift, showAttributeInfo: context.component.showAttributeInfo, @@ -2574,6 +2776,7 @@ private final class GiftViewSheetComponent: CombinedComponent { if animated { if let controller = controller() as? GiftViewScreen { controller.dismissAllTooltips() + controller.dismissBalanceOverlay() animateOut.invoke(Action { _ in controller.dismiss(completion: nil) }) @@ -2581,6 +2784,7 @@ private final class GiftViewSheetComponent: CombinedComponent { } else { if let controller = controller() as? GiftViewScreen { controller.dismissAllTooltips() + controller.dismissBalanceOverlay() controller.dismiss(completion: nil) } } @@ -2619,7 +2823,7 @@ private final class GiftViewSheetComponent: CombinedComponent { public class GiftViewScreen: ViewControllerComponentContainer { public enum Subject: Equatable { case message(EngineMessage) - case uniqueGift(StarGift.UniqueGift) + case uniqueGift(StarGift.UniqueGift, EnginePeer.Id?) case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift) case soldOutGift(StarGift.Gift) case upgradePreview([StarGift.UniqueGift.Attribute], String) @@ -2667,8 +2871,8 @@ public class GiftViewScreen: ViewControllerComponentContainer { return nil } } - case let .uniqueGift(gift), let .wearPreview(gift): - return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, nil, nil, nil) + case let .uniqueGift(gift, _), let .wearPreview(gift): + return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellStars, nil, nil) case let .profileGift(peerId, gift): var messageId: EngineMessage.Id? if case let .message(messageIdValue) = gift.reference { @@ -2715,6 +2919,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { convertToStars: (() -> Void)? = nil, transferGift: ((Bool, EnginePeer.Id) -> Signal)? = nil, upgradeGift: ((Int64?, Bool) -> Signal)? = nil, + buyGift: ((String, EnginePeer.Id) -> Signal)? = nil, updateResellStars: ((Int64?) -> Void)? = nil, togglePinnedToTop: ((Bool) -> Bool)? = nil, shareStory: ((StarGift.UniqueGift) -> Void)? = nil @@ -2732,8 +2937,9 @@ public class GiftViewScreen: ViewControllerComponentContainer { var openMyGiftsImpl: (() -> Void)? var transferGiftImpl: (() -> Void)? var upgradeGiftImpl: ((Int64?, Bool) -> Signal)? + var buyGiftImpl: ((String, EnginePeer.Id) -> Signal)? var shareGiftImpl: (() -> Void)? - var resellGiftImpl: (() -> Void)? + var resellGiftImpl: ((Bool) -> Void)? var openMoreImpl: ((ASDisplayNode, ContextGesture?) -> Void)? var showAttributeInfoImpl: ((Any, String) -> Void)? var viewUpgradedImpl: ((EngineMessage.Id) -> Void)? @@ -2763,6 +2969,9 @@ public class GiftViewScreen: ViewControllerComponentContainer { }, sendGift: { peerId in sendGiftImpl?(peerId) + }, + changeRecipient: { + }, openMyGifts: { openMyGiftsImpl?() @@ -2773,11 +2982,14 @@ public class GiftViewScreen: ViewControllerComponentContainer { upgradeGift: { formId, keepOriginalInfo in return upgradeGiftImpl?(formId, keepOriginalInfo) ?? .complete() }, + buyGift: { slug, peerId in + return buyGiftImpl?(slug, peerId) ?? .complete() + }, shareGift: { shareGiftImpl?() }, - resellGift: { - resellGiftImpl?() + resellGift: { update in + resellGiftImpl?(update) }, viewUpgraded: { messageId in viewUpgradedImpl?(messageId) @@ -3077,6 +3289,11 @@ public class GiftViewScreen: ViewControllerComponentContainer { } if let upgradeGift { return upgradeGift(formId, keepOriginalInfo) + |> afterCompleted { + if formId != nil { + context.starsContext?.load(force: true) + } + } } else { return self.context.engine.payments.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo) |> afterCompleted { @@ -3087,6 +3304,23 @@ public class GiftViewScreen: ViewControllerComponentContainer { } } + buyGiftImpl = { [weak self] slug, peerId in + guard let self else { + return .complete() + } + if let buyGift { + return buyGift(slug, peerId) + |> afterCompleted { + context.starsContext?.load(force: true) + } + } else { + return self.context.engine.payments.buyStarGift(slug: slug, peerId: peerId) + |> afterCompleted { + context.starsContext?.load(force: true) + } + } + } + shareGiftImpl = { [weak self] in guard let self, let arguments = self.subject.arguments, case let .unique(gift) = arguments.gift else { return @@ -3162,13 +3396,15 @@ public class GiftViewScreen: ViewControllerComponentContainer { self.present(shareController, in: .window(.root)) } - resellGiftImpl = { [weak self] in + resellGiftImpl = { [weak self] update in guard let self, let arguments = self.subject.arguments, case let .profileGift(peerId, currentSubject) = self.subject, case let .unique(gift) = arguments.gift else { return } + self.dismissAllTooltips() + //TODO:localize - if let resellStars = gift.resellStars, resellStars > 0 { + if let resellStars = gift.resellStars, resellStars > 0, !update { let alertController = textAlertController( context: context, title: "Unlist This Item?", @@ -3217,7 +3453,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { ) self.present(alertController, in: .window(.root)) } else { - let resellController = context.sharedContext.makeStarGiftResellScreen(context: context, completion: { [weak self] price in + let resellController = context.sharedContext.makeStarGiftResellScreen(context: context, update: update, completion: { [weak self] price in guard let self else { return } @@ -3226,7 +3462,11 @@ public class GiftViewScreen: ViewControllerComponentContainer { let giftTitle = "\(gift.title) #\(gift.number)" let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let text = "\(giftTitle) is now for sale!" + var text = "\(giftTitle) is now for sale!" + if update { + text = "\(giftTitle) is relisted for \(price) Stars." + } + let tooltipController = UndoOverlayController( presentationData: presentationData, @@ -3421,6 +3661,10 @@ public class GiftViewScreen: ViewControllerComponentContainer { view.dismissAnimated() } + self.dismissBalanceOverlay() + } + + fileprivate func dismissBalanceOverlay() { if let view = self.balanceOverlay.view, view.superview != nil { view.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4, removeOnCompletion: false) view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index d77af5a4c4..63f2308316 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -2010,7 +2010,8 @@ final class MediaEditorScreenComponent: Component { self.isSelectionPanelOpen = !self.isSelectionPanelOpen self.state?.updated() } - } + }, + animateAlpha: false )), environment: {}, containerSize: CGSize(width: 33.0, height: 33.0) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index fbb591752d..fa895d358c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -4867,6 +4867,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } return profileGifts.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo) }, + buyGift: { [weak profileGifts] slug, peerId in + guard let profileGifts else { + return .never() + } + return profileGifts.buyStarGift(slug: slug, peerId: peerId) + }, shareStory: { [weak self] uniqueGift in guard let self, let controller = self.controller else { return @@ -11118,7 +11124,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro giftsContext?.updateSorting(sorting == .date ? .value : .date) }))) - if hasPinnedGifts { + if hasPinnedGifts && hasVisibility { items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Reorder, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { _, f in diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 94d4c21e13..3ca1bfee0b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -600,6 +600,12 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } return self.profileGifts.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo) }, + buyGift: { [weak self] slug, peerId in + guard let self else { + return .never() + } + return self.profileGifts.buyStarGift(slug: slug, peerId: peerId) + }, updateResellStars: { [weak self] price in guard let self, case let .unique(uniqueGift) = product.gift else { return diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 9f39861c57..0ea0dd5b11 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -147,9 +147,9 @@ private final class SheetContent: CombinedComponent { minAmount = StarsAmount(value: 1, nanos: 0) maxAmount = withdrawConfiguration.maxPaidMediaAmount.flatMap { StarsAmount(value: $0, nanos: 0) } amountLabel = nil - case .starGiftResell: + case let .starGiftResell(update): //TODO:localize - titleString = "Sell Gift" + titleString = update ? "Edit Price" : "Sell Gift" amountTitle = "PRICE IN STARS" amountPlaceholder = "Enter Price" @@ -358,12 +358,6 @@ private final class SheetContent: CombinedComponent { buttonAttributedString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: buttonAttributedString.string)) } -// if let range = buttonAttributedString.string.range(of: "#"), let starImage = state.cachedStarImage?.0 { -// buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) -// buttonAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: buttonAttributedString.string)) -// buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string)) -// } - let button = button.update( component: ButtonComponent( background: ButtonComponent.Background( @@ -558,10 +552,11 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer { case accountWithdraw case paidMedia(Int64?) case reaction(Int64?) - case starGiftResell + case starGiftResell(Bool) } private let context: AccountContext + private let mode: StarsWithdrawScreen.Mode fileprivate let completion: (Int64) -> Void public init( @@ -570,6 +565,7 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer { completion: @escaping (Int64) -> Void ) { self.context = context + self.mode = mode self.completion = completion super.init( @@ -603,12 +599,17 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer { func presentMinAmountTooltip(_ minAmount: Int64) { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + var text = presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum(presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum_Stars(Int32(minAmount))).string + if case .starGiftResell = self.mode { + text = "You cannot sell gift for less than \(presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum_Stars(Int32(minAmount)))." + } + let resultController = UndoOverlayController( presentationData: presentationData, content: .image( image: UIImage(bundleImageName: "Premium/Stars/StarLarge")!, title: nil, - text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum(presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum_Stars(Int32(minAmount))).string, + text: text, round: false, undoText: nil ), diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Collage.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Collage.imageset/Contents.json new file mode 100644 index 0000000000..7980b2cfd4 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Collage.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "combine.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Collage.imageset/combine.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Collage.imageset/combine.pdf new file mode 100644 index 0000000000..d7fee30cd9 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/Collage.imageset/combine.pdf differ diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index b96456af3b..b2563c9c57 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2987,11 +2987,11 @@ public final class SharedAccountContextImpl: SharedAccountContext { )) controller.navigationPresentation = .modal - let _ = combineLatest( + let _ = (combineLatest( queue: Queue.mainQueue(), controller.result, - options.get() - ).startStandalone(next: { [weak controller] result, options in + options.get()) + |> take(1)).startStandalone(next: { [weak controller] result, options in if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer, let starsContext = context.starsContext { if case .starGiftTransfer = source { presentTransferAlertImpl?(EnginePeer(peer)) @@ -3275,7 +3275,6 @@ public final class SharedAccountContextImpl: SharedAccountContext { fatalError() } let controller = GiftStoreScreen(context: context, starsContext: starsContext, peerId: peerId, gift: gift) - controller.navigationPresentation = .modal return controller } @@ -3668,8 +3667,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return StarsWithdrawScreen(context: context, mode: .accountWithdraw, completion: completion) } - public func makeStarGiftResellScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController { - return StarsWithdrawScreen(context: context, mode: .starGiftResell, completion: completion) + public func makeStarGiftResellScreen(context: AccountContext, update: Bool, completion: @escaping (Int64) -> Void) -> ViewController { + return StarsWithdrawScreen(context: context, mode: .starGiftResell(update), completion: completion) } public func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController { @@ -3689,7 +3688,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { } public func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: ((StarGift.UniqueGift) -> Void)?, dismissed: (() -> Void)?) -> ViewController { - let controller = GiftViewScreen(context: context, subject: .uniqueGift(gift), shareStory: shareStory) + let controller = GiftViewScreen(context: context, subject: .uniqueGift(gift, nil), shareStory: shareStory) controller.disposed = { dismissed?() }