Various improvements

This commit is contained in:
Ilya Laktyushin 2025-04-16 13:54:14 +04:00
parent 1f051f737a
commit a43e5fc16b
23 changed files with 1617 additions and 632 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2296,7 +2296,7 @@ private final class ResaleGiftsContextImpl {
private let disposable = MetaDisposable() private let disposable = MetaDisposable()
private var sorting: ResaleGiftsContext.Sorting = .date private var sorting: ResaleGiftsContext.Sorting = .value
private var filterAttributes: [ResaleGiftsContext.Attribute] = [] private var filterAttributes: [ResaleGiftsContext.Attribute] = []
private var gifts: [StarGift] = [] private var gifts: [StarGift] = []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -53,11 +53,12 @@ private final class GiftViewSheetContent: CombinedComponent {
let convertToStars: () -> Void let convertToStars: () -> Void
let openStarsIntro: () -> Void let openStarsIntro: () -> Void
let sendGift: (EnginePeer.Id) -> Void let sendGift: (EnginePeer.Id) -> Void
let changeRecipient: () -> Void
let openMyGifts: () -> Void let openMyGifts: () -> Void
let transferGift: () -> Void let transferGift: () -> Void
let upgradeGift: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>) let upgradeGift: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)
let shareGift: () -> Void let shareGift: () -> Void
let resellGift: () -> Void let resellGift: (Bool) -> Void
let showAttributeInfo: (Any, String) -> Void let showAttributeInfo: (Any, String) -> Void
let viewUpgraded: (EngineMessage.Id) -> Void let viewUpgraded: (EngineMessage.Id) -> Void
let openMore: (ASDisplayNode, ContextGesture?) -> Void let openMore: (ASDisplayNode, ContextGesture?) -> Void
@ -74,11 +75,12 @@ private final class GiftViewSheetContent: CombinedComponent {
convertToStars: @escaping () -> Void, convertToStars: @escaping () -> Void,
openStarsIntro: @escaping () -> Void, openStarsIntro: @escaping () -> Void,
sendGift: @escaping (EnginePeer.Id) -> Void, sendGift: @escaping (EnginePeer.Id) -> Void,
changeRecipient: @escaping () -> Void,
openMyGifts: @escaping () -> Void, openMyGifts: @escaping () -> Void,
transferGift: @escaping () -> Void, transferGift: @escaping () -> Void,
upgradeGift: @escaping ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>), upgradeGift: @escaping ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>),
shareGift: @escaping () -> Void, shareGift: @escaping () -> Void,
resellGift: @escaping () -> Void, resellGift: @escaping (Bool) -> Void,
showAttributeInfo: @escaping (Any, String) -> Void, showAttributeInfo: @escaping (Any, String) -> Void,
viewUpgraded: @escaping (EngineMessage.Id) -> Void, viewUpgraded: @escaping (EngineMessage.Id) -> Void,
openMore: @escaping (ASDisplayNode, ContextGesture?) -> Void, openMore: @escaping (ASDisplayNode, ContextGesture?) -> Void,
@ -94,6 +96,7 @@ private final class GiftViewSheetContent: CombinedComponent {
self.convertToStars = convertToStars self.convertToStars = convertToStars
self.openStarsIntro = openStarsIntro self.openStarsIntro = openStarsIntro
self.sendGift = sendGift self.sendGift = sendGift
self.changeRecipient = changeRecipient
self.openMyGifts = openMyGifts self.openMyGifts = openMyGifts
self.transferGift = transferGift self.transferGift = transferGift
self.upgradeGift = upgradeGift self.upgradeGift = upgradeGift
@ -124,6 +127,13 @@ private final class GiftViewSheetContent: CombinedComponent {
private var disposable: Disposable? private var disposable: Disposable?
var initialized = false var initialized = false
var recipientPeerIdPromise = ValuePromise<EnginePeer.Id?>(nil)
var recipientPeerId: EnginePeer.Id? {
didSet {
self.recipientPeerIdPromise.set(self.recipientPeerId)
}
}
var peerMap: [EnginePeer.Id: EnginePeer] = [:] var peerMap: [EnginePeer.Id: EnginePeer] = [:]
var starGiftsMap: [Int64: StarGift.Gift] = [:] var starGiftsMap: [Int64: StarGift.Gift] = [:]
@ -187,6 +197,7 @@ private final class GiftViewSheetContent: CombinedComponent {
peerIds.append(contentsOf: media.peerIds) peerIds.append(contentsOf: media.peerIds)
} }
} }
if case let .unique(gift) = arguments.gift { if case let .unique(gift) = arguments.gift {
if case let .peerId(peerId) = gift.owner { if case let .peerId(peerId) = gift.owner {
peerIds.append(peerId) peerIds.append(peerId)
@ -247,18 +258,38 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
} }
let peerIdsSignal: Signal<[EnginePeer.Id], NoError>
if case let .uniqueGift(_, recipientPeerIdValue) = subject, let recipientPeerIdValue {
self.recipientPeerId = recipientPeerIdValue
self.recipientPeerIdPromise.set(recipientPeerIdValue)
peerIdsSignal = self.recipientPeerIdPromise.get()
|> map { recipientPeerId in
var peerIds = peerIds
if let recipientPeerId {
peerIds.append(recipientPeerId)
}
return peerIds
}
} else {
peerIdsSignal = .single(peerIds)
}
self.disposable = combineLatest(queue: Queue.mainQueue(), self.disposable = combineLatest(queue: Queue.mainQueue(),
context.engine.data.get(EngineDataMap( peerIdsSignal
|> distinctUntilChanged
|> mapToSignal { peerIds in
return context.engine.data.get(EngineDataMap(
peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in
return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
} }
)), ))
},
.single(nil) |> then(context.engine.payments.cachedStarGifts()) .single(nil) |> then(context.engine.payments.cachedStarGifts())
).startStrict(next: { [weak self] peers, starGifts in ).startStrict(next: { [weak self] peers, starGifts in
if let strongSelf = self { if let strongSelf = self {
var peersMap: [EnginePeer.Id: EnginePeer] = [:] var peersMap: [EnginePeer.Id: EnginePeer] = [:]
for peerId in peerIds { for (peerId, maybePeer) in peers {
if let maybePeer = peers[peerId], let peer = maybePeer { if let peer = maybePeer {
peersMap[peerId] = peer peersMap[peerId] = peer
} }
} }
@ -281,7 +312,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() self.optionsPromise.set(context.engine.payments.starsTopUpOptions()
|> map(Optional.init)) |> map(Optional.init))
} }
@ -289,7 +325,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if let controller = getController() as? GiftViewScreen { if let controller = getController() as? GiftViewScreen {
controller.updateSubject.connect { [weak self] subject in controller.updateSubject.connect { [weak self] subject in
self?.subject = subject self?.subject = subject
self?.updated() self?.updated(transition: .easeInOut(duration: 0.25))
} }
} }
} }
@ -359,17 +395,47 @@ 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() { 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 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 action = {
let proceed: (Int64) -> Void = { formId in let proceed: (Int64) -> Void = { formId in
self.inProgress = true self.inProgress = true
self.updated() self.updated()
let signal = self.context.engine.payments.sendStarsPaymentForm(formId: formId, source: .starGiftResale(slug: uniqueGift.slug, toPeerId: self.context.account.peerId)) let signal = context.engine.payments.sendStarsPaymentForm(formId: formId, source: .starGiftResale(slug: uniqueGift.slug, toPeerId: recipientPeerId))
|> mapError { _ -> SendBotPaymentFormError in |> mapError { _ -> SendBotPaymentFormError in
return .generic return .generic
} }
@ -388,6 +454,77 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
self.inProgress = false 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() controller.animateSuccess()
self.updated(transition: .spring(duration: 0.4)) self.updated(transition: .spring(duration: 0.4))
@ -432,13 +569,25 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
} }
} }
let giftTitle = "\(uniqueGift.title) #\(uniqueGift.number)"
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: recipientPeerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
}
let text: String
if recipientPeerId == self.context.account.peerId {
text = "Do you really want to buy **\(giftTitle)** for **\(resellStars)** Stars?"
} else {
text = "Do you really want to buy **\(giftTitle)** for **\(resellStars)** Stars and gift it to **\(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))**?"
}
let alertController = textAlertController( let alertController = textAlertController(
context: self.context, context: self.context,
title: "Confirm Payment", title: "Confirm Payment",
text: "Do you really want to buy **\(giftTitle)** for **\(arguments.resellStars ?? 0)** Stars?", text: text,
actions: [ actions: [
TextAlertAction(type: .defaultAction, title: "Buy for \(arguments.resellStars ?? 0) Stars", action: { TextAlertAction(type: .defaultAction, title: "Buy for \(resellStars) Stars", action: {
action() action()
}), }),
TextAlertAction(type: .genericAction, title: "Cancel", action: { TextAlertAction(type: .genericAction, title: "Cancel", action: {
@ -450,6 +599,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if let controller = self.getController() as? GiftViewScreen { if let controller = self.getController() as? GiftViewScreen {
controller.present(alertController, in: .window(.root)) controller.present(alertController, in: .window(.root))
} }
})
} }
func commitUpgrade() { func commitUpgrade() {
@ -531,6 +681,8 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
static var body: Body { static var body: Body {
let priceButton = Child(PlainButtonComponent.self)
let buttons = Child(ButtonsComponent.self) let buttons = Child(ButtonsComponent.self)
let animation = Child(GiftCompositionComponent.self) let animation = Child(GiftCompositionComponent.self)
let title = Child(MultilineTextComponent.self) let title = Child(MultilineTextComponent.self)
@ -1148,12 +1300,7 @@ private final class GiftViewSheetContent: CombinedComponent {
var descriptionText: String var descriptionText: String
if let uniqueGift { if let uniqueGift {
titleString = uniqueGift.title 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 { } else if soldOut {
descriptionText = strings.Gift_View_UnavailableDescription descriptionText = strings.Gift_View_UnavailableDescription
} else if upgraded { } else if upgraded {
@ -1341,6 +1488,43 @@ private final class GiftViewSheetContent: CombinedComponent {
if !soldOut { if !soldOut {
if let uniqueGift { if let uniqueGift {
if case let .uniqueGift(_, recipientPeerIdValue) = component.subject, let _ = recipientPeerIdValue, let recipientPeerId = state.recipientPeerId {
//TODO:localize
if let peer = state.peerMap[recipientPeerId] {
tableItems.append(.init(
id: "recipient",
title: "Recipient",
component: AnyComponent(
Button(
content: AnyComponent(
HStack([
AnyComponentWithIdentity(
id: AnyHashable(peer.id),
component: AnyComponent(PeerCellComponent(
context: component.context,
theme: theme,
strings: strings,
peer: peer
))
),
AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(ButtonContentComponent(
context: component.context,
text: "change",
color: theme.list.itemAccentColor
))
)
], spacing: 4.0)
),
action: { [weak state] in
state?.changeRecipient()
}
)
)
))
}
} else {
switch uniqueGift.owner { switch uniqueGift.owner {
case let .peerId(peerId): case let .peerId(peerId):
if let peer = state.peerMap[peerId] { if let peer = state.peerMap[peerId] {
@ -1478,6 +1662,7 @@ private final class GiftViewSheetContent: CombinedComponent {
) )
)) ))
} }
}
} else if let peerId = subject.arguments?.fromPeerId, let peer = state.peerMap[peerId] { } else if let peerId = subject.arguments?.fromPeerId, let peer = state.peerMap[peerId] {
var isBot = false var isBot = false
if case let .user(user) = peer, user.botInfo != nil { if case let .user(user) = peer, user.botInfo != nil {
@ -1679,7 +1864,7 @@ private final class GiftViewSheetContent: CombinedComponent {
), ),
effectAlignment: .center, effectAlignment: .center,
action: { action: {
component.resellGift() component.resellGift(false)
} }
), ),
environment: {}, environment: {},
@ -1691,29 +1876,6 @@ private final class GiftViewSheetContent: CombinedComponent {
.appear(.default(scale: true, alpha: true)) .appear(.default(scale: true, alpha: true))
.disappear(.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 let showAttributeInfo = component.showAttributeInfo
@ -2066,17 +2228,48 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
var resellStars: Int64? var resellStars: Int64?
var selling = false
if let uniqueGift { if let uniqueGift {
resellStars = uniqueGift.resellStars resellStars = uniqueGift.resellStars
if incoming, let resellStars {
let priceButton = priceButton.update(
component: PlainButtonComponent(
content: AnyComponent(
PriceButtonComponent(price: resellStars)
),
effectAlignment: .center,
action: {
component.resellGift(true)
},
animateScale: false
),
availableSize: CGSize(width: 120.0, height: 30.0),
transition: context.transition
)
context.add(priceButton
.position(CGPoint(x: environment.safeInsets.left + 16.0 + priceButton.size.width / 2.0, y: 28.0))
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
} }
if ((incoming && !converted && !upgraded) || exported || (!incoming && resellStars != nil)) && (!showUpgradePreview && !showWearPreview) {
if !incoming, let _ = resellStars {
if case let .uniqueGift(_, recipientPeerId) = component.subject, recipientPeerId != nil {
} else {
selling = true
}
}
}
if ((incoming && !converted && !upgraded) || exported || selling) && (!showUpgradePreview && !showWearPreview) {
let linkColor = theme.actionSheet.controlAccentColor let linkColor = theme.actionSheet.controlAccentColor
if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme { if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme {
state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme) state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme)
} }
var addressToOpen: String? var addressToOpen: String?
var descriptionText: String var descriptionText: String
if let uniqueGift, !incoming { if let uniqueGift, selling {
//TODO:localize //TODO:localize
let ownerName: String let ownerName: String
if case let .peerId(peerId) = uniqueGift.owner { if case let .peerId(peerId) = uniqueGift.owner {
@ -2381,6 +2574,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme { if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme {
state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme) state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme)
} }
//TODO:localize
var upgradeString = "Buy for" var upgradeString = "Buy for"
upgradeString += " # \(presentationStringsFormattedNumber(Int32(resellStars), environment.dateTimeFormat.groupingSeparator))" upgradeString += " # \(presentationStringsFormattedNumber(Int32(resellStars), environment.dateTimeFormat.groupingSeparator))"
@ -2454,11 +2648,12 @@ private final class GiftViewSheetComponent: CombinedComponent {
let convertToStars: () -> Void let convertToStars: () -> Void
let openStarsIntro: () -> Void let openStarsIntro: () -> Void
let sendGift: (EnginePeer.Id) -> Void let sendGift: (EnginePeer.Id) -> Void
let changeRecipient: () -> Void
let openMyGifts: () -> Void let openMyGifts: () -> Void
let transferGift: () -> Void let transferGift: () -> Void
let upgradeGift: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>) let upgradeGift: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)
let shareGift: () -> Void let shareGift: () -> Void
let resellGift: () -> Void let resellGift: (Bool) -> Void
let viewUpgraded: (EngineMessage.Id) -> Void let viewUpgraded: (EngineMessage.Id) -> Void
let openMore: (ASDisplayNode, ContextGesture?) -> Void let openMore: (ASDisplayNode, ContextGesture?) -> Void
let showAttributeInfo: (Any, String) -> Void let showAttributeInfo: (Any, String) -> Void
@ -2473,11 +2668,12 @@ private final class GiftViewSheetComponent: CombinedComponent {
convertToStars: @escaping () -> Void, convertToStars: @escaping () -> Void,
openStarsIntro: @escaping () -> Void, openStarsIntro: @escaping () -> Void,
sendGift: @escaping (EnginePeer.Id) -> Void, sendGift: @escaping (EnginePeer.Id) -> Void,
changeRecipient: @escaping () -> Void,
openMyGifts: @escaping () -> Void, openMyGifts: @escaping () -> Void,
transferGift: @escaping () -> Void, transferGift: @escaping () -> Void,
upgradeGift: @escaping ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>), upgradeGift: @escaping ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>),
shareGift: @escaping () -> Void, shareGift: @escaping () -> Void,
resellGift: @escaping () -> Void, resellGift: @escaping (Bool) -> Void,
viewUpgraded: @escaping (EngineMessage.Id) -> Void, viewUpgraded: @escaping (EngineMessage.Id) -> Void,
openMore: @escaping (ASDisplayNode, ContextGesture?) -> Void, openMore: @escaping (ASDisplayNode, ContextGesture?) -> Void,
showAttributeInfo: @escaping (Any, String) -> Void showAttributeInfo: @escaping (Any, String) -> Void
@ -2491,6 +2687,7 @@ private final class GiftViewSheetComponent: CombinedComponent {
self.convertToStars = convertToStars self.convertToStars = convertToStars
self.openStarsIntro = openStarsIntro self.openStarsIntro = openStarsIntro
self.sendGift = sendGift self.sendGift = sendGift
self.changeRecipient = changeRecipient
self.openMyGifts = openMyGifts self.openMyGifts = openMyGifts
self.transferGift = transferGift self.transferGift = transferGift
self.upgradeGift = upgradeGift self.upgradeGift = upgradeGift
@ -2542,6 +2739,7 @@ private final class GiftViewSheetComponent: CombinedComponent {
convertToStars: context.component.convertToStars, convertToStars: context.component.convertToStars,
openStarsIntro: context.component.openStarsIntro, openStarsIntro: context.component.openStarsIntro,
sendGift: context.component.sendGift, sendGift: context.component.sendGift,
changeRecipient: context.component.changeRecipient,
openMyGifts: context.component.openMyGifts, openMyGifts: context.component.openMyGifts,
transferGift: context.component.transferGift, transferGift: context.component.transferGift,
upgradeGift: context.component.upgradeGift, upgradeGift: context.component.upgradeGift,
@ -2619,7 +2817,7 @@ private final class GiftViewSheetComponent: CombinedComponent {
public class GiftViewScreen: ViewControllerComponentContainer { public class GiftViewScreen: ViewControllerComponentContainer {
public enum Subject: Equatable { public enum Subject: Equatable {
case message(EngineMessage) case message(EngineMessage)
case uniqueGift(StarGift.UniqueGift) case uniqueGift(StarGift.UniqueGift, EnginePeer.Id?)
case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift) case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift)
case soldOutGift(StarGift.Gift) case soldOutGift(StarGift.Gift)
case upgradePreview([StarGift.UniqueGift.Attribute], String) case upgradePreview([StarGift.UniqueGift.Attribute], String)
@ -2667,8 +2865,8 @@ public class GiftViewScreen: ViewControllerComponentContainer {
return nil return nil
} }
} }
case let .uniqueGift(gift), let .wearPreview(gift): 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) 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): case let .profileGift(peerId, gift):
var messageId: EngineMessage.Id? var messageId: EngineMessage.Id?
if case let .message(messageIdValue) = gift.reference { if case let .message(messageIdValue) = gift.reference {
@ -2733,7 +2931,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
var transferGiftImpl: (() -> Void)? var transferGiftImpl: (() -> Void)?
var upgradeGiftImpl: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)? var upgradeGiftImpl: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)?
var shareGiftImpl: (() -> Void)? var shareGiftImpl: (() -> Void)?
var resellGiftImpl: (() -> Void)? var resellGiftImpl: ((Bool) -> Void)?
var openMoreImpl: ((ASDisplayNode, ContextGesture?) -> Void)? var openMoreImpl: ((ASDisplayNode, ContextGesture?) -> Void)?
var showAttributeInfoImpl: ((Any, String) -> Void)? var showAttributeInfoImpl: ((Any, String) -> Void)?
var viewUpgradedImpl: ((EngineMessage.Id) -> Void)? var viewUpgradedImpl: ((EngineMessage.Id) -> Void)?
@ -2763,6 +2961,9 @@ public class GiftViewScreen: ViewControllerComponentContainer {
}, },
sendGift: { peerId in sendGift: { peerId in
sendGiftImpl?(peerId) sendGiftImpl?(peerId)
},
changeRecipient: {
}, },
openMyGifts: { openMyGifts: {
openMyGiftsImpl?() openMyGiftsImpl?()
@ -2776,8 +2977,8 @@ public class GiftViewScreen: ViewControllerComponentContainer {
shareGift: { shareGift: {
shareGiftImpl?() shareGiftImpl?()
}, },
resellGift: { resellGift: { update in
resellGiftImpl?() resellGiftImpl?(update)
}, },
viewUpgraded: { messageId in viewUpgraded: { messageId in
viewUpgradedImpl?(messageId) viewUpgradedImpl?(messageId)
@ -3162,13 +3363,15 @@ public class GiftViewScreen: ViewControllerComponentContainer {
self.present(shareController, in: .window(.root)) 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 { guard let self, let arguments = self.subject.arguments, case let .profileGift(peerId, currentSubject) = self.subject, case let .unique(gift) = arguments.gift else {
return return
} }
self.dismissAllTooltips()
//TODO:localize //TODO:localize
if let resellStars = gift.resellStars, resellStars > 0 { if let resellStars = gift.resellStars, resellStars > 0, !update {
let alertController = textAlertController( let alertController = textAlertController(
context: context, context: context,
title: "Unlist This Item?", title: "Unlist This Item?",
@ -3217,7 +3420,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
) )
self.present(alertController, in: .window(.root)) self.present(alertController, in: .window(.root))
} else { } 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 { guard let self else {
return return
} }
@ -3226,7 +3429,11 @@ public class GiftViewScreen: ViewControllerComponentContainer {
let giftTitle = "\(gift.title) #\(gift.number)" let giftTitle = "\(gift.title) #\(gift.number)"
let presentationData = context.sharedContext.currentPresentationData.with { $0 } 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( let tooltipController = UndoOverlayController(
presentationData: presentationData, presentationData: presentationData,

View File

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

View File

@ -11118,7 +11118,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
giftsContext?.updateSorting(sorting == .date ? .value : .date) giftsContext?.updateSorting(sorting == .date ? .value : .date)
}))) })))
if hasPinnedGifts { if hasPinnedGifts && hasVisibility {
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Reorder, icon: { theme in items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Reorder, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor)
}, action: { _, f in }, action: { _, f in

View File

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

View File

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

View File

@ -3273,7 +3273,6 @@ public final class SharedAccountContextImpl: SharedAccountContext {
fatalError() fatalError()
} }
let controller = GiftStoreScreen(context: context, starsContext: starsContext, peerId: peerId, gift: gift) let controller = GiftStoreScreen(context: context, starsContext: starsContext, peerId: peerId, gift: gift)
controller.navigationPresentation = .modal
return controller return controller
} }
@ -3666,8 +3665,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return StarsWithdrawScreen(context: context, mode: .accountWithdraw, completion: completion) return StarsWithdrawScreen(context: context, mode: .accountWithdraw, completion: completion)
} }
public func makeStarGiftResellScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController { public func makeStarGiftResellScreen(context: AccountContext, update: Bool, completion: @escaping (Int64) -> Void) -> ViewController {
return StarsWithdrawScreen(context: context, mode: .starGiftResell, completion: completion) return StarsWithdrawScreen(context: context, mode: .starGiftResell(update), completion: completion)
} }
public func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController { public func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController {
@ -3687,7 +3686,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
} }
public func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: ((StarGift.UniqueGift) -> Void)?, dismissed: (() -> Void)?) -> ViewController { public func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: ((StarGift.UniqueGift) -> Void)?, dismissed: (() -> Void)?) -> ViewController {
let controller = GiftViewScreen(context: context, subject: .uniqueGift(gift), shareStory: shareStory) let controller = GiftViewScreen(context: context, subject: .uniqueGift(gift, nil), shareStory: shareStory)
controller.disposed = { controller.disposed = {
dismissed?() dismissed?()
} }

View File

@ -899,6 +899,7 @@ public final class OngoingGroupCallContext {
} }
return OngoingGroupCallRequestedVideoChannel( return OngoingGroupCallRequestedVideoChannel(
audioSsrc: channel.audioSsrc, audioSsrc: channel.audioSsrc,
userId: channel.peerId,
endpointId: channel.endpointId, endpointId: channel.endpointId,
ssrcGroups: channel.ssrcGroups.map { group in ssrcGroups: channel.ssrcGroups.map { group in
return OngoingGroupCallSsrcGroup( return OngoingGroupCallSsrcGroup(