mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
[WIP] Gift resale
This commit is contained in:
parent
da86483d66
commit
b130511450
@ -653,6 +653,7 @@ public enum ChatListSearchFilter: Equatable {
|
||||
case files
|
||||
case music
|
||||
case voice
|
||||
case instantVideo
|
||||
case peer(PeerId, Bool, String, String)
|
||||
case date(Int32?, Int32, String)
|
||||
case publicPosts
|
||||
@ -679,8 +680,10 @@ public enum ChatListSearchFilter: Equatable {
|
||||
return 8
|
||||
case .voice:
|
||||
return 9
|
||||
case .publicPosts:
|
||||
case .instantVideo:
|
||||
return 10
|
||||
case .publicPosts:
|
||||
return 11
|
||||
case let .peer(peerId, _, _, _):
|
||||
return peerId.id._internalGetInt64Value()
|
||||
case let .date(_, date, _):
|
||||
@ -1126,6 +1129,7 @@ public protocol SharedAccountContext: AnyObject {
|
||||
func makeStarsGiftController(context: AccountContext, birthdays: [EnginePeer.Id: TelegramBirthday]?, completion: @escaping (([EnginePeer.Id]) -> Void)) -> ViewController
|
||||
func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Signal<Never, TransferStarGiftError>)?) -> ViewController
|
||||
func makeGiftOptionsController(context: AccountContext, peerId: EnginePeer.Id, premiumOptions: [CachedPremiumGiftOption], hasBirthday: Bool, completion: (() -> Void)?) -> ViewController
|
||||
func makeGiftStoreController(context: AccountContext, peerId: EnginePeer.Id, gift: StarGift.Gift) -> ViewController
|
||||
func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController
|
||||
func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, subject: BoostSubject, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController
|
||||
|
||||
@ -1169,6 +1173,7 @@ public protocol SharedAccountContext: AnyObject {
|
||||
func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController
|
||||
func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController
|
||||
func makeStarsWithdrawalScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController
|
||||
func makeStarGiftResellScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController
|
||||
func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController
|
||||
func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> ViewController
|
||||
func makeStarsIntroScreen(context: AccountContext) -> ViewController
|
||||
@ -1435,7 +1440,10 @@ public struct StarsSubscriptionConfiguration {
|
||||
usdWithdrawRate: 1200,
|
||||
paidMessageMaxAmount: 10000,
|
||||
paidMessageCommissionPermille: 850,
|
||||
paidMessagesAvailable: false
|
||||
paidMessagesAvailable: false,
|
||||
starGiftResaleMinAmount: 125,
|
||||
starGiftResaleMaxAmount: 3500,
|
||||
starGiftCommissionPermille: 80
|
||||
)
|
||||
}
|
||||
|
||||
@ -1444,19 +1452,28 @@ public struct StarsSubscriptionConfiguration {
|
||||
public let paidMessageMaxAmount: Int64
|
||||
public let paidMessageCommissionPermille: Int32
|
||||
public let paidMessagesAvailable: Bool
|
||||
public let starGiftResaleMinAmount: Int64
|
||||
public let starGiftResaleMaxAmount: Int64
|
||||
public let starGiftCommissionPermille: Int32
|
||||
|
||||
fileprivate init(
|
||||
maxFee: Int64,
|
||||
usdWithdrawRate: Int64,
|
||||
paidMessageMaxAmount: Int64,
|
||||
paidMessageCommissionPermille: Int32,
|
||||
paidMessagesAvailable: Bool
|
||||
paidMessagesAvailable: Bool,
|
||||
starGiftResaleMinAmount: Int64,
|
||||
starGiftResaleMaxAmount: Int64,
|
||||
starGiftCommissionPermille: Int32
|
||||
) {
|
||||
self.maxFee = maxFee
|
||||
self.usdWithdrawRate = usdWithdrawRate
|
||||
self.paidMessageMaxAmount = paidMessageMaxAmount
|
||||
self.paidMessageCommissionPermille = paidMessageCommissionPermille
|
||||
self.paidMessagesAvailable = paidMessagesAvailable
|
||||
self.starGiftResaleMinAmount = starGiftResaleMinAmount
|
||||
self.starGiftResaleMaxAmount = starGiftResaleMaxAmount
|
||||
self.starGiftCommissionPermille = starGiftCommissionPermille
|
||||
}
|
||||
|
||||
public static func with(appConfiguration: AppConfiguration) -> StarsSubscriptionConfiguration {
|
||||
@ -1466,13 +1483,19 @@ public struct StarsSubscriptionConfiguration {
|
||||
let paidMessageMaxAmount = (data["stars_paid_message_amount_max"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.paidMessageMaxAmount
|
||||
let paidMessageCommissionPermille = (data["stars_paid_message_commission_permille"] as? Double).flatMap(Int32.init) ?? StarsSubscriptionConfiguration.defaultValue.paidMessageCommissionPermille
|
||||
let paidMessagesAvailable = (data["stars_paid_messages_available"] as? Bool) ?? StarsSubscriptionConfiguration.defaultValue.paidMessagesAvailable
|
||||
let starGiftResaleMinAmount = (data["stars_stargift_resale_amount_min"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.starGiftResaleMinAmount
|
||||
let starGiftResaleMaxAmount = (data["stars_stargift_resale_amount_max"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.starGiftResaleMaxAmount
|
||||
let starGiftCommissionPermille = (data["stars_stargift_resale_commission_permille"] as? Double).flatMap(Int32.init) ?? StarsSubscriptionConfiguration.defaultValue.starGiftCommissionPermille
|
||||
|
||||
return StarsSubscriptionConfiguration(
|
||||
maxFee: maxFee,
|
||||
usdWithdrawRate: usdWithdrawRate,
|
||||
paidMessageMaxAmount: paidMessageMaxAmount,
|
||||
paidMessageCommissionPermille: paidMessageCommissionPermille,
|
||||
paidMessagesAvailable: paidMessagesAvailable
|
||||
paidMessagesAvailable: paidMessagesAvailable,
|
||||
starGiftResaleMinAmount: starGiftResaleMinAmount,
|
||||
starGiftResaleMaxAmount: starGiftResaleMaxAmount,
|
||||
starGiftCommissionPermille: starGiftCommissionPermille
|
||||
)
|
||||
} else {
|
||||
return .defaultValue
|
||||
|
@ -608,7 +608,7 @@ public struct ChatTextInputStateText: Codable, Equatable {
|
||||
return lhs.text == rhs.text && lhs.attributes == rhs.attributes
|
||||
}
|
||||
|
||||
public func attributedText() -> NSAttributedString {
|
||||
public func attributedText(files: [Int64: TelegramMediaFile] = [:]) -> NSAttributedString {
|
||||
let result = NSMutableAttributedString(string: self.text)
|
||||
for attribute in self.attributes {
|
||||
switch attribute.type {
|
||||
@ -623,7 +623,7 @@ public struct ChatTextInputStateText: Codable, Equatable {
|
||||
case let .textUrl(url):
|
||||
result.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||
case let .customEmoji(_, fileId, enableAnimation):
|
||||
result.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil, enableAnimation: enableAnimation), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||
result.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: files[fileId], enableAnimation: enableAnimation), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||
case .strikethrough:
|
||||
result.addAttribute(ChatTextInputAttributes.strikethrough, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||
case .underline:
|
||||
|
@ -357,6 +357,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
key = .music
|
||||
case .voice:
|
||||
key = .voice
|
||||
case .instantVideo:
|
||||
key = .instantVideo
|
||||
case .publicPosts:
|
||||
key = .publicPosts
|
||||
case let .date(minDate, maxDate, title):
|
||||
@ -685,6 +687,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
filterKey = .music
|
||||
case .voice:
|
||||
filterKey = .voice
|
||||
case .instantVideo:
|
||||
filterKey = .instantVideo
|
||||
case .publicPosts:
|
||||
filterKey = .publicPosts
|
||||
}
|
||||
@ -725,6 +729,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
key = .music
|
||||
case .voice:
|
||||
key = .voice
|
||||
case .instantVideo:
|
||||
key = .instantVideo
|
||||
case .downloads:
|
||||
key = .downloads
|
||||
default:
|
||||
|
@ -108,6 +108,9 @@ private final class ItemNode: ASDisplayNode {
|
||||
case .voice:
|
||||
title = presentationData.strings.ChatList_Search_FilterVoice
|
||||
icon = nil
|
||||
case .instantVideo:
|
||||
title = presentationData.strings.ChatList_Search_FilterVoice
|
||||
icon = nil
|
||||
case .publicPosts:
|
||||
title = presentationData.strings.ChatList_Search_FilterPublicPosts
|
||||
icon = nil
|
||||
|
@ -1587,8 +1587,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
|
||||
private let recentListNode: ListView
|
||||
private let shimmerNode: ChatListSearchShimmerNode
|
||||
private let listNode: ListView
|
||||
private let mediaNode: ChatListSearchMediaNode
|
||||
private let listNode: ListView?
|
||||
private let mediaNode: ChatListSearchMediaNode?
|
||||
private var enqueuedRecentTransitions: [(ChatListSearchContainerRecentTransition, Bool)] = []
|
||||
private var enqueuedTransitions: [(ChatListSearchContainerTransition, Bool)] = []
|
||||
|
||||
@ -1714,6 +1714,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
tagMask = .music
|
||||
case .voice:
|
||||
tagMask = .voiceOrInstantVideo
|
||||
case .instantVideo:
|
||||
tagMask = .roundVideo
|
||||
}
|
||||
self.tagMask = tagMask
|
||||
|
||||
@ -1737,8 +1739,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
self.shimmerNode.allowsGroupOpacity = true
|
||||
|
||||
self.listNode = ListView()
|
||||
self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
|
||||
self.listNode.accessibilityPageScrolledString = { row, count in
|
||||
self.listNode?.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
|
||||
self.listNode?.accessibilityPageScrolledString = { row, count in
|
||||
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
|
||||
}
|
||||
|
||||
@ -1746,13 +1748,17 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
var transitionNodeImpl: ((EngineMessage.Id, EngineMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?)?
|
||||
var addToTransitionSurfaceImpl: ((UIView) -> Void)?
|
||||
|
||||
self.mediaNode = ChatListSearchMediaNode(context: self.context, contentType: .photoOrVideo, openMessage: { message, mode in
|
||||
openMediaMessageImpl?(EngineMessage(message), mode)
|
||||
}, messageContextAction: { message, node, rect, gesture in
|
||||
interaction.mediaMessageContextAction(EngineMessage(message), node, rect, gesture)
|
||||
}, toggleMessageSelection: { messageId, selected in
|
||||
interaction.toggleMessageSelection(messageId, selected)
|
||||
})
|
||||
if key == .media {
|
||||
self.mediaNode = ChatListSearchMediaNode(context: self.context, contentType: .photoOrVideo, openMessage: { message, mode in
|
||||
openMediaMessageImpl?(EngineMessage(message), mode)
|
||||
}, messageContextAction: { message, node, rect, gesture in
|
||||
interaction.mediaMessageContextAction(EngineMessage(message), node, rect, gesture)
|
||||
}, toggleMessageSelection: { messageId, selected in
|
||||
interaction.toggleMessageSelection(messageId, selected)
|
||||
})
|
||||
} else {
|
||||
self.mediaNode = nil
|
||||
}
|
||||
|
||||
self.mediaAccessoryPanelContainer = PassthroughContainerNode()
|
||||
self.mediaAccessoryPanelContainer.clipsToBounds = true
|
||||
@ -1822,8 +1828,12 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
self.addSubnode(recentEmptyNode)
|
||||
}
|
||||
|
||||
self.addSubnode(self.listNode)
|
||||
self.addSubnode(self.mediaNode)
|
||||
if let listNode = self.listNode {
|
||||
self.addSubnode(listNode)
|
||||
}
|
||||
if let mediaNode = self.mediaNode {
|
||||
self.addSubnode(mediaNode)
|
||||
}
|
||||
|
||||
self.addSubnode(self.emptyResultsAnimationNode)
|
||||
self.addSubnode(self.emptyResultsTitleNode)
|
||||
@ -1850,8 +1860,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
}
|
||||
}
|
||||
|
||||
self.listNode.isHidden = true
|
||||
self.mediaNode.isHidden = true
|
||||
self.listNode?.isHidden = true
|
||||
self.mediaNode?.isHidden = true
|
||||
self.recentListNode.isHidden = peersFilter.contains(.excludeRecent)
|
||||
|
||||
let currentRemotePeers = Atomic<([FoundPeer], [FoundPeer], [AdPeer])?>(value: nil)
|
||||
@ -3227,16 +3237,16 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
}
|
||||
|
||||
transitionNodeImpl = { [weak self] messageId, media in
|
||||
if let strongSelf = self {
|
||||
return strongSelf.mediaNode.transitionNodeForGallery(messageId: messageId, media: media._asMedia())
|
||||
if let self {
|
||||
return self.mediaNode?.transitionNodeForGallery(messageId: messageId, media: media._asMedia())
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
addToTransitionSurfaceImpl = { [weak self] view in
|
||||
if let strongSelf = self {
|
||||
strongSelf.mediaNode.addToTransitionSurface(view: view)
|
||||
if let self {
|
||||
self.mediaNode?.addToTransitionSurface(view: view)
|
||||
}
|
||||
}
|
||||
|
||||
@ -3270,7 +3280,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
case .savedMessagesChats:
|
||||
break
|
||||
}
|
||||
self?.listNode.clearHighlightAnimated(true)
|
||||
self?.listNode?.clearHighlightAnimated(true)
|
||||
}, disabledPeerSelected: { _, _, _ in
|
||||
}, togglePeerSelected: { _, _ in
|
||||
}, togglePeersSelection: { _, _ in
|
||||
@ -3280,12 +3290,12 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
if let strongSelf = self, let peer = message.peers[message.id.peerId] {
|
||||
interaction.openMessage(EnginePeer(peer), threadId, message.id, strongSelf.key == .chats)
|
||||
}
|
||||
self?.listNode.clearHighlightAnimated(true)
|
||||
self?.listNode?.clearHighlightAnimated(true)
|
||||
}, groupSelected: { _ in
|
||||
}, addContact: { [weak self] phoneNumber in
|
||||
interaction.dismissInput()
|
||||
interaction.addContact(phoneNumber)
|
||||
self?.listNode.clearHighlightAnimated(true)
|
||||
self?.listNode?.clearHighlightAnimated(true)
|
||||
}, setPeerIdWithRevealedOptions: { _, _ in
|
||||
}, setItemPinned: { _, _ in
|
||||
}, setPeerMuted: { _, _ in
|
||||
@ -3328,7 +3338,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
}
|
||||
interaction.dismissInput()
|
||||
interaction.openPeer(peer, peer, threadId, false)
|
||||
self.listNode.clearHighlightAnimated(true)
|
||||
self.listNode?.clearHighlightAnimated(true)
|
||||
})
|
||||
}, openStorageManagement: {
|
||||
}, openPasswordSetup: {
|
||||
@ -3397,7 +3407,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
}, transitionNode: { messageId, media, _ in
|
||||
var transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
|
||||
if let strongSelf = self {
|
||||
strongSelf.listNode.forEachItemNode { itemNode in
|
||||
strongSelf.listNode?.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ListMessageNode {
|
||||
if let result = itemNode.transitionNode(id: messageId, media: media, adjustRect: false) {
|
||||
transitionNode = result
|
||||
@ -3556,7 +3566,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
if isSearching && (entries?.isEmpty ?? true) {
|
||||
entries = nil
|
||||
}
|
||||
strongSelf.mediaNode.updateHistory(entries: entries, totalCount: 0, updateType: .Initial)
|
||||
strongSelf.mediaNode?.updateHistory(entries: entries, totalCount: 0, updateType: .Initial)
|
||||
} else if strongSelf.tagMask == .roundVideo {
|
||||
|
||||
}
|
||||
|
||||
var peers: [EnginePeer] = []
|
||||
@ -4349,7 +4361,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
if strongSelf.backgroundColor != nil {
|
||||
strongSelf.backgroundColor = presentationData.theme.chatList.backgroundColor
|
||||
}
|
||||
strongSelf.listNode.forEachItemHeaderNode({ itemHeaderNode in
|
||||
strongSelf.listNode?.forEachItemHeaderNode({ itemHeaderNode in
|
||||
if let itemHeaderNode = itemHeaderNode as? ChatListSearchItemHeaderNode {
|
||||
itemHeaderNode.updateTheme(theme: presentationData.theme)
|
||||
}
|
||||
@ -4367,26 +4379,26 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
interaction.dismissInput()
|
||||
}
|
||||
|
||||
self.listNode.beganInteractiveDragging = { _ in
|
||||
self.listNode?.beganInteractiveDragging = { _ in
|
||||
interaction.dismissInput()
|
||||
}
|
||||
|
||||
self.mediaNode.beganInteractiveDragging = {
|
||||
self.mediaNode?.beganInteractiveDragging = {
|
||||
interaction.dismissInput()
|
||||
}
|
||||
|
||||
self.listNode.visibleBottomContentOffsetChanged = { offset in
|
||||
self.listNode?.visibleBottomContentOffsetChanged = { offset in
|
||||
guard case let .known(value) = offset, value < 160.0 else {
|
||||
return
|
||||
}
|
||||
loadMore()
|
||||
}
|
||||
|
||||
self.mediaNode.loadMore = {
|
||||
self.mediaNode?.loadMore = {
|
||||
loadMore()
|
||||
}
|
||||
|
||||
if [.file, .music, .voiceOrInstantVideo].contains(tagMask) || self.key == .downloads {
|
||||
if [.file, .music, .voiceOrInstantVideo, .voice, .roundVideo].contains(tagMask) || self.key == .downloads {
|
||||
let key = self.key
|
||||
self.mediaStatusDisposable = (context.sharedContext.mediaManager.globalMediaPlayerState
|
||||
|> mapToSignal { playlistStateAndType -> Signal<(Account, SharedMediaPlayerItemPlaybackState, MediaManagerPlayerType)?, NoError> in
|
||||
@ -4396,7 +4408,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
if let playlistId = state.playlistId as? PeerMessagesMediaPlaylistId, case .custom = playlistId {
|
||||
switch type {
|
||||
case .voice:
|
||||
if tagMask != .voiceOrInstantVideo {
|
||||
if ![.voiceOrInstantVideo, .voice, .roundVideo].contains(tagMask) {
|
||||
return .single(nil) |> delay(0.2, queue: .mainQueue())
|
||||
}
|
||||
case .music:
|
||||
@ -4524,8 +4536,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
}
|
||||
|
||||
func scrollToTop() -> Bool {
|
||||
if !self.mediaNode.isHidden {
|
||||
return self.mediaNode.scrollToTop()
|
||||
if let mediaNode = self.mediaNode, !mediaNode.isHidden {
|
||||
return mediaNode.scrollToTop()
|
||||
} else if !self.recentListNode.isHidden {
|
||||
let offset = self.recentListNode.visibleContentOffset()
|
||||
switch offset {
|
||||
@ -4535,15 +4547,17 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
let offset = self.listNode.visibleContentOffset()
|
||||
} else if let listNode = self.listNode {
|
||||
let offset = listNode.visibleContentOffset()
|
||||
switch offset {
|
||||
case let .known(value) where value <= CGFloat.ulpOfOne:
|
||||
return false
|
||||
default:
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -4852,11 +4866,11 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
emptyRecentAnimationNode.updateLayout(size: emptyRecentAnimationSize)
|
||||
}
|
||||
|
||||
self.listNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
self.listNode?.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.listNode?.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
self.mediaNode.frame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: size.width, height: size.height))
|
||||
self.mediaNode.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: false, expandProgress: 1.0, presentationData: self.presentationData, synchronous: true, transition: transition)
|
||||
self.mediaNode?.frame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: size.width, height: size.height))
|
||||
self.mediaNode?.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: false, expandProgress: 1.0, presentationData: self.presentationData, synchronous: true, transition: transition)
|
||||
|
||||
do {
|
||||
let padding: CGFloat = 16.0
|
||||
@ -4887,7 +4901,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
}
|
||||
|
||||
func updateHiddenMedia() {
|
||||
self.listNode.forEachItemNode { itemNode in
|
||||
self.listNode?.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ListMessageNode {
|
||||
itemNode.updateHiddenMedia()
|
||||
}
|
||||
@ -4899,7 +4913,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
|
||||
func transitionNodeForGallery(messageId: EngineMessage.Id, media: EngineMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
||||
var transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
|
||||
self.listNode.forEachItemNode { itemNode in
|
||||
self.listNode?.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ListMessageNode {
|
||||
if let result = itemNode.transitionNode(id: messageId, media: media._asMedia(), adjustRect: false) {
|
||||
transitionNode = result
|
||||
@ -4915,8 +4929,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
|
||||
func updateSelectedMessages(animated: Bool) {
|
||||
self.selectedMessages = self.interaction.getSelectedMessageIds()
|
||||
self.mediaNode.selectedMessageIds = self.selectedMessages
|
||||
self.mediaNode.updateSelectedMessages(animated: animated)
|
||||
self.mediaNode?.selectedMessageIds = self.selectedMessages
|
||||
self.mediaNode?.updateSelectedMessages(animated: animated)
|
||||
}
|
||||
|
||||
func removeAds() {
|
||||
@ -5006,16 +5020,16 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
options.insert(.PreferSynchronousResourceLoading)
|
||||
}
|
||||
|
||||
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
|
||||
self.listNode?.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
let searchOptions = strongSelf.searchOptionsValue
|
||||
strongSelf.listNode.isHidden = strongSelf.tagMask == .photoOrVideo && (strongSelf.searchQueryValue ?? "").isEmpty
|
||||
strongSelf.mediaNode.isHidden = !strongSelf.listNode.isHidden
|
||||
strongSelf.listNode?.isHidden = strongSelf.tagMask == .photoOrVideo && (strongSelf.searchQueryValue ?? "").isEmpty
|
||||
strongSelf.mediaNode?.isHidden = !(strongSelf.listNode?.isHidden ?? true)
|
||||
|
||||
let displayingResults = transition.displayingResults
|
||||
if !displayingResults {
|
||||
strongSelf.listNode.isHidden = true
|
||||
strongSelf.mediaNode.isHidden = true
|
||||
strongSelf.listNode?.isHidden = true
|
||||
strongSelf.mediaNode?.isHidden = true
|
||||
}
|
||||
|
||||
let emptyResults = displayingResults && transition.isEmpty
|
||||
@ -5103,7 +5117,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
}
|
||||
} else {
|
||||
let adjustedLocation = self.convert(location, to: self.listNode)
|
||||
self.listNode.forEachItemNode { itemNode in
|
||||
self.listNode?.forEachItemNode { itemNode in
|
||||
if itemNode.frame.contains(adjustedLocation) {
|
||||
selectedItemNode = itemNode
|
||||
}
|
||||
@ -5506,7 +5520,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode {
|
||||
)
|
||||
|
||||
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false) : .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true)
|
||||
case .voice:
|
||||
case .voice, .instantVideo:
|
||||
var media: [EngineMedia] = []
|
||||
media.append(.file(TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: [.Audio(isVoice: true, duration: 0, title: nil, performer: nil, waveform: Data())], alternativeRepresentations: [])))
|
||||
let message = EngineMessage(
|
||||
|
@ -61,6 +61,7 @@ public enum ChatListSearchPaneKey {
|
||||
case files
|
||||
case music
|
||||
case voice
|
||||
case instantVideo
|
||||
}
|
||||
|
||||
extension ChatListSearchPaneKey {
|
||||
@ -88,6 +89,8 @@ extension ChatListSearchPaneKey {
|
||||
return .music
|
||||
case .voice:
|
||||
return .voice
|
||||
case .instantVideo:
|
||||
return .instantVideo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -126,6 +126,7 @@ public final class ContextMenuActionItem {
|
||||
public let id: AnyHashable?
|
||||
public let text: String
|
||||
public let entities: [MessageTextEntity]
|
||||
public let entityFiles: [Int64: TelegramMediaFile]
|
||||
public let enableEntityAnimations: Bool
|
||||
public let textColor: ContextMenuActionItemTextColor
|
||||
public let textFont: ContextMenuActionItemFont
|
||||
@ -147,6 +148,7 @@ public final class ContextMenuActionItem {
|
||||
id: AnyHashable? = nil,
|
||||
text: String,
|
||||
entities: [MessageTextEntity] = [],
|
||||
entityFiles: [Int64: TelegramMediaFile] = [:],
|
||||
enableEntityAnimations: Bool = true,
|
||||
textColor: ContextMenuActionItemTextColor = .primary,
|
||||
textLayout: ContextMenuActionItemTextLayout = .twoLinesMax,
|
||||
@ -168,6 +170,7 @@ public final class ContextMenuActionItem {
|
||||
id: id,
|
||||
text: text,
|
||||
entities: entities,
|
||||
entityFiles: entityFiles,
|
||||
enableEntityAnimations: enableEntityAnimations,
|
||||
textColor: textColor,
|
||||
textLayout: textLayout,
|
||||
@ -199,6 +202,7 @@ public final class ContextMenuActionItem {
|
||||
id: AnyHashable? = nil,
|
||||
text: String,
|
||||
entities: [MessageTextEntity] = [],
|
||||
entityFiles: [Int64: TelegramMediaFile] = [:],
|
||||
enableEntityAnimations: Bool = true,
|
||||
textColor: ContextMenuActionItemTextColor = .primary,
|
||||
textLayout: ContextMenuActionItemTextLayout = .twoLinesMax,
|
||||
@ -219,6 +223,7 @@ public final class ContextMenuActionItem {
|
||||
self.id = id
|
||||
self.text = text
|
||||
self.entities = entities
|
||||
self.entityFiles = entityFiles
|
||||
self.enableEntityAnimations = enableEntityAnimations
|
||||
self.textColor = textColor
|
||||
self.textFont = textFont
|
||||
|
@ -361,14 +361,21 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
|
||||
let inputStateText = ChatTextInputStateText(text: self.item.text, attributes: self.item.entities.compactMap { entity -> ChatTextInputStateTextAttribute? in
|
||||
if case let .CustomEmoji(_, fileId) = entity.type {
|
||||
return ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: fileId, enableAnimation: true), range: entity.range)
|
||||
} else if case .Bold = entity.type {
|
||||
return ChatTextInputStateTextAttribute(type: .bold, range: entity.range)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
let result = NSMutableAttributedString(attributedString: inputStateText.attributedText())
|
||||
let result = NSMutableAttributedString(attributedString: inputStateText.attributedText(files: self.item.entityFiles))
|
||||
result.addAttributes([
|
||||
.font: titleFont,
|
||||
.foregroundColor: titleColor
|
||||
], range: NSRange(location: 0, length: result.length))
|
||||
for attribute in inputStateText.attributes {
|
||||
if case .bold = attribute.type {
|
||||
result.addAttribute(NSAttributedString.Key.font, value: Font.semibold(15.0), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||
}
|
||||
}
|
||||
attributedText = result
|
||||
} else {
|
||||
attributedText = parseMarkdownIntoAttributedString(
|
||||
|
@ -437,8 +437,8 @@ public extension CALayer {
|
||||
self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.size.height", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
|
||||
}
|
||||
|
||||
func animateBoundsOriginXAdditive(from: CGFloat, to: CGFloat, duration: Double, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
|
||||
self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.x", timingFunction: timingFunction, duration: duration, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion)
|
||||
func animateBoundsOriginXAdditive(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
|
||||
self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.x", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion)
|
||||
}
|
||||
|
||||
func animateBoundsOriginYAdditive(from: CGFloat, to: CGFloat, duration: Double, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
|
||||
|
@ -1155,7 +1155,7 @@ private final class ProfileGiftsContextImpl {
|
||||
if !filter.contains(.unique) {
|
||||
flags |= (1 << 4)
|
||||
}
|
||||
return network.request(Api.functions.payments.getSavedStarGifts(flags: flags, peer: inputPeer, offset: initialNextOffset ?? "", limit: 32))
|
||||
return network.request(Api.functions.payments.getSavedStarGifts(flags: flags, peer: inputPeer, offset: initialNextOffset ?? "", limit: 36))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.payments.SavedStarGifts?, NoError> in
|
||||
return .single(nil)
|
||||
@ -2220,3 +2220,283 @@ public extension StarGift.UniqueGift {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private final class ResaleGiftsContextImpl {
|
||||
private let queue: Queue
|
||||
private let account: Account
|
||||
private let giftId: Int64
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
private var sorting: ResaleGiftsContext.Sorting = .date
|
||||
private var filterAttributes: [ResaleGiftsContext.Attribute] = []
|
||||
|
||||
private var gifts: [StarGift] = []
|
||||
private var attributes: [StarGift.UniqueGift.Attribute] = []
|
||||
private var attributeCount: [ResaleGiftsContext.Attribute: Int32] = [:]
|
||||
|
||||
private var count: Int32?
|
||||
private var dataState: ResaleGiftsContext.State.DataState = .ready(canLoadMore: true, nextOffset: nil)
|
||||
|
||||
var _state: ResaleGiftsContext.State?
|
||||
private let stateValue = Promise<ResaleGiftsContext.State>()
|
||||
var state: Signal<ResaleGiftsContext.State, NoError> {
|
||||
return self.stateValue.get()
|
||||
}
|
||||
|
||||
init(
|
||||
queue: Queue,
|
||||
account: Account,
|
||||
giftId: Int64
|
||||
) {
|
||||
self.queue = queue
|
||||
self.account = account
|
||||
self.giftId = giftId
|
||||
|
||||
self.loadMore()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
func reload() {
|
||||
self.gifts = []
|
||||
self.dataState = .ready(canLoadMore: true, nextOffset: nil)
|
||||
self.loadMore(reload: true)
|
||||
}
|
||||
|
||||
func loadMore(reload: Bool = false) {
|
||||
let giftId = self.giftId
|
||||
let accountPeerId = self.account.peerId
|
||||
let network = self.account.network
|
||||
let postbox = self.account.postbox
|
||||
let sorting = self.sorting
|
||||
let filterAttributes = self.filterAttributes
|
||||
|
||||
let dataState = self.dataState
|
||||
|
||||
if case let .ready(true, initialNextOffset) = dataState {
|
||||
self.dataState = .loading
|
||||
if !reload {
|
||||
self.pushState()
|
||||
}
|
||||
|
||||
var flags: Int32 = 0
|
||||
switch sorting {
|
||||
case .date:
|
||||
break
|
||||
case .value:
|
||||
flags |= (1 << 1)
|
||||
case .number:
|
||||
flags |= (1 << 2)
|
||||
}
|
||||
|
||||
var apiAttributes: [Api.StarGiftAttributeId]?
|
||||
if !filterAttributes.isEmpty {
|
||||
flags |= (1 << 3)
|
||||
apiAttributes = filterAttributes.map {
|
||||
switch $0 {
|
||||
case let .model(id):
|
||||
return .starGiftAttributeIdModel(documentId: id)
|
||||
case let .pattern(id):
|
||||
return .starGiftAttributeIdPattern(documentId: id)
|
||||
case let .backdrop(id):
|
||||
return .starGiftAttributeIdBackdrop(backdropId: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var attributesHash: Int64?
|
||||
if "".isEmpty {
|
||||
flags |= (1 << 0)
|
||||
attributesHash = 0
|
||||
}
|
||||
|
||||
let signal = network.request(Api.functions.payments.getResaleStarGifts(flags: flags, attributesHash: attributesHash, giftId: giftId, attributes: apiAttributes, offset: initialNextOffset ?? "", limit: 36))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.payments.ResaleStarGifts?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<([StarGift], [StarGift.UniqueGift.Attribute], [ResaleGiftsContext.Attribute: Int32], Int32, String?), NoError> in
|
||||
guard let result else {
|
||||
return .single(([], [], [:], 0, nil))
|
||||
}
|
||||
return postbox.transaction { transaction -> ([StarGift], [StarGift.UniqueGift.Attribute], [ResaleGiftsContext.Attribute: Int32], Int32, String?) in
|
||||
switch result {
|
||||
case let .resaleStarGifts(_, count, gifts, nextOffset, attributes, attributesHash, chats, counters, users):
|
||||
let _ = attributesHash
|
||||
|
||||
var resultAttributes: [StarGift.UniqueGift.Attribute] = []
|
||||
if let attributes {
|
||||
resultAttributes = attributes.compactMap { StarGift.UniqueGift.Attribute(apiAttribute: $0) }
|
||||
}
|
||||
|
||||
var attributeCount: [ResaleGiftsContext.Attribute: Int32] = [:]
|
||||
if let counters {
|
||||
for counter in counters {
|
||||
switch counter {
|
||||
case let .starGiftAttributeCounter(attribute, count):
|
||||
switch attribute {
|
||||
case let .starGiftAttributeIdModel(documentId):
|
||||
attributeCount[.model(documentId)] = count
|
||||
case let .starGiftAttributeIdPattern(documentId):
|
||||
attributeCount[.pattern(documentId)] = count
|
||||
case let .starGiftAttributeIdBackdrop(backdropId):
|
||||
attributeCount[.backdrop(backdropId)] = count
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
|
||||
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
|
||||
return (gifts.compactMap { StarGift(apiStarGift: $0) }, resultAttributes, attributeCount, count, nextOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.disposable.set((signal
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] (gifts, attributes, attributeCount, count, nextOffset) in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if initialNextOffset == nil || reload {
|
||||
self.gifts = gifts
|
||||
} else {
|
||||
for gift in gifts {
|
||||
self.gifts.append(gift)
|
||||
}
|
||||
}
|
||||
|
||||
let updatedCount = max(Int32(self.gifts.count), count)
|
||||
self.count = updatedCount
|
||||
self.attributes = attributes
|
||||
if !attributeCount.isEmpty {
|
||||
self.attributeCount = attributeCount
|
||||
}
|
||||
self.dataState = .ready(canLoadMore: count != 0 && updatedCount > self.gifts.count && nextOffset != nil, nextOffset: nextOffset)
|
||||
|
||||
self.pushState()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
func updateFilterAttributes(_ filterAttributes: [ResaleGiftsContext.Attribute]) {
|
||||
guard self.filterAttributes != filterAttributes else {
|
||||
return
|
||||
}
|
||||
self.filterAttributes = filterAttributes
|
||||
self.dataState = .ready(canLoadMore: true, nextOffset: nil)
|
||||
self.pushState()
|
||||
|
||||
self.loadMore()
|
||||
}
|
||||
|
||||
func updateSorting(_ sorting: ResaleGiftsContext.Sorting) {
|
||||
guard self.sorting != sorting else {
|
||||
return
|
||||
}
|
||||
self.sorting = sorting
|
||||
self.dataState = .ready(canLoadMore: true, nextOffset: nil)
|
||||
self.pushState()
|
||||
|
||||
self.loadMore()
|
||||
}
|
||||
|
||||
private func pushState() {
|
||||
let state = ResaleGiftsContext.State(
|
||||
sorting: self.sorting,
|
||||
filterAttributes: self.filterAttributes,
|
||||
gifts: self.gifts,
|
||||
attributes: self.attributes,
|
||||
attributeCount: self.attributeCount,
|
||||
count: self.count,
|
||||
dataState: self.dataState
|
||||
)
|
||||
self._state = state
|
||||
self.stateValue.set(.single(state))
|
||||
}
|
||||
}
|
||||
|
||||
public final class ResaleGiftsContext {
|
||||
public enum Sorting: Equatable {
|
||||
case date
|
||||
case value
|
||||
case number
|
||||
}
|
||||
|
||||
public enum Attribute: Equatable, Hashable {
|
||||
case model(Int64)
|
||||
case pattern(Int64)
|
||||
case backdrop(Int32)
|
||||
}
|
||||
|
||||
public struct State: Equatable {
|
||||
public enum DataState: Equatable {
|
||||
case loading
|
||||
case ready(canLoadMore: Bool, nextOffset: String?)
|
||||
}
|
||||
|
||||
public var sorting: Sorting
|
||||
public var filterAttributes: [Attribute]
|
||||
public var gifts: [StarGift]
|
||||
public var attributes: [StarGift.UniqueGift.Attribute]
|
||||
public var attributeCount: [Attribute: Int32]
|
||||
public var count: Int32?
|
||||
public var dataState: ResaleGiftsContext.State.DataState
|
||||
}
|
||||
|
||||
private let queue: Queue = .mainQueue()
|
||||
private let impl: QueueLocalObject<ResaleGiftsContextImpl>
|
||||
|
||||
public var state: Signal<ResaleGiftsContext.State, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.state.start(next: { value in
|
||||
subscriber.putNext(value)
|
||||
}))
|
||||
}
|
||||
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
public init(
|
||||
account: Account,
|
||||
giftId: Int64
|
||||
) {
|
||||
let queue = self.queue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return ResaleGiftsContextImpl(queue: queue, account: account, giftId: giftId)
|
||||
})
|
||||
}
|
||||
|
||||
public func loadMore() {
|
||||
self.impl.with { impl in
|
||||
impl.loadMore()
|
||||
}
|
||||
}
|
||||
|
||||
public func updateSorting(_ sorting: ResaleGiftsContext.Sorting) {
|
||||
self.impl.with { impl in
|
||||
impl.updateSorting(sorting)
|
||||
}
|
||||
}
|
||||
|
||||
public func updateFilterAttributes(_ attributes: [ResaleGiftsContext.Attribute]) {
|
||||
self.impl.with { impl in
|
||||
impl.updateFilterAttributes(attributes)
|
||||
}
|
||||
}
|
||||
|
||||
public var currentState: ResaleGiftsContext.State? {
|
||||
var state: ResaleGiftsContext.State?
|
||||
self.impl.syncWith { impl in
|
||||
state = impl._state
|
||||
}
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
@ -1149,7 +1149,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, starsPrice)._tuple, body: bodyAttributes, argumentAttributes: attributes)
|
||||
}
|
||||
}
|
||||
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, peerId, senderId, _):
|
||||
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, peerId, senderId, _, _):
|
||||
if case let .unique(gift) = gift {
|
||||
if !forAdditionalServiceMessage && !"".isEmpty {
|
||||
attributedString = NSAttributedString(string: "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))", font: titleFont, textColor: primaryTextColor)
|
||||
|
@ -463,6 +463,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/MiniAppListScreen",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsIntroScreen",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftStoreScreen",
|
||||
"//submodules/TelegramUI/Components/ContentReportScreen",
|
||||
"//submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent",
|
||||
@ -472,6 +473,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/Components/BlurredBackgroundComponent",
|
||||
"//submodules/TelegramUI/Components/CheckComponent",
|
||||
"//submodules/TelegramUI/Components/MarqueeComponent",
|
||||
"//third-party/recaptcha:RecaptchaEnterprise",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
|
@ -560,7 +560,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
buttonTitle = item.presentationData.strings.Notification_StarGift_View
|
||||
}
|
||||
}
|
||||
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _):
|
||||
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _):
|
||||
if case let .unique(uniqueGift) = gift {
|
||||
isStarGift = true
|
||||
|
||||
@ -594,7 +594,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
case let .model(name, file, _):
|
||||
modelValue = name
|
||||
animationFile = file
|
||||
case let .backdrop(name, innerColor, outerColor, patternColor, _, _):
|
||||
case let .backdrop(name, _, innerColor, outerColor, patternColor, _, _):
|
||||
uniqueBackgroundColor = UIColor(rgb: UInt32(bitPattern: outerColor))
|
||||
uniqueSecondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColor))
|
||||
uniquePatternColor = UIColor(rgb: UInt32(bitPattern: patternColor))
|
||||
|
@ -2189,7 +2189,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
||||
context: context,
|
||||
theme: presentationData.theme.theme,
|
||||
strings: presentationData.strings,
|
||||
subject: .uniqueGift(gift: gift),
|
||||
subject: .uniqueGift(gift: gift, price: nil),
|
||||
mode: .preview
|
||||
)
|
||||
),
|
||||
|
@ -128,7 +128,7 @@ public final class EntityKeyboardAnimationData: Equatable {
|
||||
for attribute in gift.attributes {
|
||||
if case let .model(_, fileValue, _) = attribute {
|
||||
file = fileValue
|
||||
} else if case let .backdrop(_, innerColor, outerColor, _, _, _) = attribute {
|
||||
} else if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute {
|
||||
color = UIColor(rgb: UInt32(bitPattern: innerColor))
|
||||
let _ = outerColor
|
||||
}
|
||||
|
@ -149,6 +149,11 @@ public final class GiftCompositionComponent: Component {
|
||||
previewTimer.invalidate()
|
||||
self.previewTimer = nil
|
||||
}
|
||||
|
||||
if !self.fetchedFiles.contains(file.fileId.id) {
|
||||
self.disposables.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
||||
self.fetchedFiles.insert(file.fileId.id)
|
||||
}
|
||||
case let .unique(gift):
|
||||
for attribute in gift.attributes {
|
||||
switch attribute {
|
||||
@ -161,7 +166,7 @@ public final class GiftCompositionComponent: Component {
|
||||
case let .pattern(_, file, _):
|
||||
patternFile = file
|
||||
files[file.fileId.id] = file
|
||||
case let .backdrop(_, innerColorValue, outerColorValue, patternColorValue, _, _):
|
||||
case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, _, _):
|
||||
backgroundColor = UIColor(rgb: UInt32(bitPattern: outerColorValue))
|
||||
secondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColorValue))
|
||||
patternColor = UIColor(rgb: UInt32(bitPattern: patternColorValue))
|
||||
@ -222,7 +227,7 @@ public final class GiftCompositionComponent: Component {
|
||||
files[file.fileId.id] = file
|
||||
}
|
||||
|
||||
if case let .backdrop(_, innerColorValue, outerColorValue, patternColorValue, _, _) = self.previewBackdrops[Int(self.previewBackdropIndex)] {
|
||||
if case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, _, _) = self.previewBackdrops[Int(self.previewBackdropIndex)] {
|
||||
backgroundColor = UIColor(rgb: UInt32(bitPattern: outerColorValue))
|
||||
secondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColorValue))
|
||||
patternColor = UIColor(rgb: UInt32(bitPattern: patternColorValue))
|
||||
|
@ -20,7 +20,7 @@ public final class GiftItemComponent: Component {
|
||||
public enum Subject: Equatable {
|
||||
case premium(months: Int32, price: String)
|
||||
case starGift(gift: StarGift.Gift, price: String)
|
||||
case uniqueGift(gift: StarGift.UniqueGift)
|
||||
case uniqueGift(gift: StarGift.UniqueGift, price: String?)
|
||||
}
|
||||
|
||||
public struct Ribbon: Equatable {
|
||||
@ -28,6 +28,7 @@ public final class GiftItemComponent: Component {
|
||||
case red
|
||||
case blue
|
||||
case purple
|
||||
case green
|
||||
case custom(Int32, Int32)
|
||||
|
||||
func colors(theme: PresentationTheme) -> [UIColor] {
|
||||
@ -61,6 +62,11 @@ public final class GiftItemComponent: Component {
|
||||
UIColor(rgb: 0x747bf6),
|
||||
UIColor(rgb: 0xe367d8)
|
||||
]
|
||||
case .green:
|
||||
return [
|
||||
UIColor(rgb: 0x4bb121),
|
||||
UIColor(rgb: 0x53d654)
|
||||
]
|
||||
case let .custom(topColor, _):
|
||||
return [
|
||||
UIColor(rgb: UInt32(bitPattern: topColor)).withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89),
|
||||
@ -72,17 +78,25 @@ public final class GiftItemComponent: Component {
|
||||
|
||||
public enum Font {
|
||||
case generic
|
||||
case larger
|
||||
case monospaced
|
||||
}
|
||||
|
||||
public let text: String
|
||||
public let font: Font
|
||||
public let color: Color
|
||||
public let outline: UIColor?
|
||||
|
||||
public init(text: String, font: Font = .generic, color: Color) {
|
||||
public init(
|
||||
text: String,
|
||||
font: Font = .generic,
|
||||
color: Color,
|
||||
outline: UIColor? = nil
|
||||
) {
|
||||
self.text = text
|
||||
self.font = font
|
||||
self.color = color
|
||||
self.outline = outline
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,6 +122,7 @@ public final class GiftItemComponent: Component {
|
||||
let subtitle: String?
|
||||
let label: String?
|
||||
let ribbon: Ribbon?
|
||||
let resellPrice: Int64?
|
||||
let isLoading: Bool
|
||||
let isHidden: Bool
|
||||
let isSoldOut: Bool
|
||||
@ -128,6 +143,7 @@ public final class GiftItemComponent: Component {
|
||||
subtitle: String? = nil,
|
||||
label: String? = nil,
|
||||
ribbon: Ribbon? = nil,
|
||||
resellPrice: Int64? = nil,
|
||||
isLoading: Bool = false,
|
||||
isHidden: Bool = false,
|
||||
isSoldOut: Bool = false,
|
||||
@ -147,6 +163,7 @@ public final class GiftItemComponent: Component {
|
||||
self.subtitle = subtitle
|
||||
self.label = label
|
||||
self.ribbon = ribbon
|
||||
self.resellPrice = resellPrice
|
||||
self.isLoading = isLoading
|
||||
self.isHidden = isHidden
|
||||
self.isSoldOut = isSoldOut
|
||||
@ -186,6 +203,9 @@ public final class GiftItemComponent: Component {
|
||||
if lhs.ribbon != rhs.ribbon {
|
||||
return false
|
||||
}
|
||||
if lhs.resellPrice != rhs.resellPrice {
|
||||
return false
|
||||
}
|
||||
if lhs.isLoading != rhs.isLoading {
|
||||
return false
|
||||
}
|
||||
@ -229,6 +249,8 @@ public final class GiftItemComponent: Component {
|
||||
private let subtitle = ComponentView<Empty>()
|
||||
private let button = ComponentView<Empty>()
|
||||
private let label = ComponentView<Empty>()
|
||||
|
||||
private let ribbonOutline = UIImageView()
|
||||
private let ribbon = UIImageView()
|
||||
private let ribbonText = ComponentView<Empty>()
|
||||
|
||||
@ -244,6 +266,9 @@ public final class GiftItemComponent: Component {
|
||||
private var hiddenIcon: UIImageView?
|
||||
private var pinnedIcon: UIImageView?
|
||||
|
||||
private var resellBackground: BlurredBackgroundView?
|
||||
private let reselLabel = ComponentView<Empty>()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
@ -383,7 +408,7 @@ public final class GiftItemComponent: Component {
|
||||
file: gift.file
|
||||
)
|
||||
animationOffset = 16.0
|
||||
case let .uniqueGift(gift):
|
||||
case let .uniqueGift(gift, _):
|
||||
animationOffset = 16.0
|
||||
for attribute in gift.attributes {
|
||||
switch attribute {
|
||||
@ -396,7 +421,7 @@ public final class GiftItemComponent: Component {
|
||||
case let .pattern(_, file, _):
|
||||
patternFile = file
|
||||
files[file.fileId.id] = file
|
||||
case let .backdrop(_, innerColorValue, outerColorValue, patternColorValue, _, _):
|
||||
case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, _, _):
|
||||
backgroundColor = UIColor(rgb: UInt32(bitPattern: outerColorValue))
|
||||
secondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColorValue))
|
||||
patternColor = UIColor(rgb: UInt32(bitPattern: patternColorValue))
|
||||
@ -530,6 +555,7 @@ public final class GiftItemComponent: Component {
|
||||
|
||||
let buttonColor: UIColor
|
||||
var starsColor: UIColor?
|
||||
var tinted = false
|
||||
let price: String
|
||||
switch component.subject {
|
||||
case let .premium(_, priceValue), let .starGift(_, priceValue):
|
||||
@ -542,9 +568,10 @@ public final class GiftItemComponent: Component {
|
||||
buttonColor = component.theme.list.itemAccentColor
|
||||
}
|
||||
price = priceValue
|
||||
case .uniqueGift:
|
||||
case let .uniqueGift(_, priceValue):
|
||||
buttonColor = UIColor.white
|
||||
price = component.strings.Gift_Options_Gift_Transfer
|
||||
price = priceValue ?? component.strings.Gift_Options_Gift_Transfer
|
||||
tinted = true
|
||||
}
|
||||
|
||||
let buttonSize = self.button.update(
|
||||
@ -554,6 +581,7 @@ public final class GiftItemComponent: Component {
|
||||
context: component.context,
|
||||
text: price,
|
||||
color: buttonColor,
|
||||
tinted: tinted,
|
||||
starsColor: starsColor
|
||||
)
|
||||
),
|
||||
@ -623,6 +651,8 @@ public final class GiftItemComponent: Component {
|
||||
switch ribbon.font {
|
||||
case .generic:
|
||||
ribbonFont = Font.semibold(ribbonFontSize)
|
||||
case .larger:
|
||||
ribbonFont = Font.semibold(10.0)
|
||||
case .monospaced:
|
||||
ribbonFont = Font.with(size: 10.0, design: .monospace, weight: .semibold)
|
||||
}
|
||||
@ -645,6 +675,18 @@ public final class GiftItemComponent: Component {
|
||||
}
|
||||
ribbonTextView.bounds = CGRect(origin: .zero, size: ribbonTextSize)
|
||||
|
||||
if let _ = component.ribbon?.outline {
|
||||
if self.ribbonOutline.image == nil || themeUpdated || previousComponent?.ribbon?.outline != component.ribbon?.outline {
|
||||
self.ribbonOutline.image = ribbonOutlineImage
|
||||
self.ribbonOutline.tintColor = component.ribbon?.outline
|
||||
if self.ribbonOutline.superview == nil {
|
||||
self.insertSubview(self.ribbonOutline, belowSubview: self.ribbon)
|
||||
}
|
||||
}
|
||||
} else if self.ribbonOutline.superview != nil {
|
||||
self.ribbonOutline.removeFromSuperview()
|
||||
}
|
||||
|
||||
if self.ribbon.image == nil || themeUpdated || previousComponent?.ribbon?.color != component.ribbon?.color {
|
||||
var direction: GradientImageDirection = .mirroredDiagonal
|
||||
if case .custom = ribbon.color {
|
||||
@ -661,11 +703,16 @@ public final class GiftItemComponent: Component {
|
||||
if let ribbonImage = self.ribbon.image {
|
||||
self.ribbon.frame = CGRect(origin: CGPoint(x: size.width - ribbonImage.size.width + ribbonOffset.x, y: ribbonOffset.y), size: ribbonImage.size)
|
||||
}
|
||||
if let ribbonOutlineImage = self.ribbonOutline.image {
|
||||
self.ribbonOutline.frame = ribbonOutlineImage.size.centered(around: self.ribbon.center.offsetBy(dx: 0.0, dy: 2.0))
|
||||
}
|
||||
|
||||
ribbonTextView.transform = CGAffineTransform(rotationAngle: .pi / 4.0)
|
||||
ribbonTextView.center = CGPoint(x: size.width - 22.0 + ribbonOffset.x, y: 22.0 + ribbonOffset.y)
|
||||
}
|
||||
} else {
|
||||
if self.ribbonText.view?.superview != nil {
|
||||
self.ribbonOutline.removeFromSuperview()
|
||||
self.ribbon.removeFromSuperview()
|
||||
self.ribbonText.view?.removeFromSuperview()
|
||||
}
|
||||
@ -808,6 +855,72 @@ public final class GiftItemComponent: Component {
|
||||
hiddenIcon.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
|
||||
if let resellPrice = component.resellPrice {
|
||||
let labelColor = UIColor.white
|
||||
let attributes = MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: Font.semibold(11.0), textColor: labelColor),
|
||||
bold: MarkdownAttributeSet(font: Font.semibold(11.0), textColor: labelColor),
|
||||
link: MarkdownAttributeSet(font: Font.regular(11.0), textColor: labelColor),
|
||||
linkAttribute: { contents in
|
||||
return (TelegramTextAttributes.URL, contents)
|
||||
}
|
||||
)
|
||||
let labelText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("#\(resellPrice)", attributes: attributes))
|
||||
if let range = labelText.string.range(of: "#") {
|
||||
labelText.addAttribute(NSAttributedString.Key.font, value: Font.semibold(10.0), range: NSRange(range, in: labelText.string))
|
||||
labelText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: NSRange(range, in: labelText.string))
|
||||
}
|
||||
|
||||
let resellSize = self.reselLabel.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
MultilineTextWithEntitiesComponent(
|
||||
context: component.context,
|
||||
animationCache: component.context.animationCache,
|
||||
animationRenderer: component.context.animationRenderer,
|
||||
placeholderColor: .white,
|
||||
text: .plain(labelText),
|
||||
horizontalAlignment: .center
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
|
||||
let resellFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - resellSize.width) / 2.0), y: size.height - 20.0), size: resellSize)
|
||||
|
||||
let resellBackground: BlurredBackgroundView
|
||||
var resellBackgroundTransition = transition
|
||||
if let currentBackground = self.resellBackground {
|
||||
resellBackground = currentBackground
|
||||
} else {
|
||||
resellBackgroundTransition = .immediate
|
||||
|
||||
resellBackground = BlurredBackgroundView(color: UIColor(rgb: 0x000000, alpha: 0.3), enableBlur: true) //UIVisualEffectView(effect: blurEffect)
|
||||
resellBackground.clipsToBounds = true
|
||||
self.resellBackground = resellBackground
|
||||
|
||||
self.addSubview(resellBackground)
|
||||
}
|
||||
let resellBackgroundFrame = resellFrame.insetBy(dx: -6.0, dy: -4.0)
|
||||
resellBackgroundTransition.setFrame(view: resellBackground, frame: resellBackgroundFrame)
|
||||
resellBackground.update(size: resellBackgroundFrame.size, cornerRadius: resellBackgroundFrame.size.height / 2.0, transition: resellBackgroundTransition.containedViewLayoutTransition)
|
||||
|
||||
if let resellLabelView = self.reselLabel.view {
|
||||
if resellLabelView.superview == nil {
|
||||
self.addSubview(resellLabelView)
|
||||
}
|
||||
transition.setFrame(view: resellLabelView, frame: resellFrame)
|
||||
}
|
||||
} else {
|
||||
self.reselLabel.view?.removeFromSuperview()
|
||||
if let resellBackground = self.resellBackground {
|
||||
self.resellBackground = nil
|
||||
resellBackground.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
if case .grid = component.mode {
|
||||
let lineWidth: CGFloat = 2.0
|
||||
let selectionFrame = backgroundFrame.insetBy(dx: 3.0, dy: 3.0)
|
||||
@ -873,17 +986,20 @@ private final class ButtonContentComponent: Component {
|
||||
let context: AccountContext
|
||||
let text: String
|
||||
let color: UIColor
|
||||
let tinted: Bool
|
||||
let starsColor: UIColor?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
text: String,
|
||||
color: UIColor,
|
||||
tinted: Bool = false,
|
||||
starsColor: UIColor? = nil
|
||||
) {
|
||||
self.context = context
|
||||
self.text = text
|
||||
self.color = color
|
||||
self.tinted = tinted
|
||||
self.starsColor = starsColor
|
||||
}
|
||||
|
||||
@ -897,6 +1013,9 @@ private final class ButtonContentComponent: Component {
|
||||
if lhs.color != rhs.color {
|
||||
return false
|
||||
}
|
||||
if lhs.tinted != rhs.tinted {
|
||||
return false
|
||||
}
|
||||
if lhs.starsColor != rhs.starsColor {
|
||||
return false
|
||||
}
|
||||
@ -930,7 +1049,7 @@ private final class ButtonContentComponent: Component {
|
||||
let attributedText = NSMutableAttributedString(string: component.text, font: Font.semibold(11.0), textColor: component.color)
|
||||
let range = (attributedText.string as NSString).range(of: "⭐️")
|
||||
if range.location != NSNotFound {
|
||||
attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
|
||||
attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: component.tinted)), range: range)
|
||||
attributedText.addAttribute(.font, value: Font.semibold(15.0), range: range)
|
||||
attributedText.addAttribute(.baselineOffset, value: 2.0, range: NSRange(location: range.upperBound, length: attributedText.length - range.upperBound))
|
||||
}
|
||||
@ -1057,3 +1176,11 @@ private final class StarsButtonEffectLayer: SimpleLayer {
|
||||
self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
private var ribbonOutlineImage: UIImage? = {
|
||||
if let image = UIImage(bundleImageName: "Premium/GiftRibbon") {
|
||||
return generateScaledImage(image: image, size: CGSize(width: image.size.width + 8.0, height: image.size.height + 8.0), opaque: false)?.withRenderingMode(.alwaysTemplate)
|
||||
} else {
|
||||
return UIImage()
|
||||
}
|
||||
}()
|
||||
|
@ -79,6 +79,7 @@ final class GiftOptionsScreenComponent: Component {
|
||||
case all
|
||||
case limited
|
||||
case inStock
|
||||
case resale
|
||||
case stars(Int64)
|
||||
case transfer
|
||||
|
||||
@ -92,6 +93,8 @@ final class GiftOptionsScreenComponent: Component {
|
||||
self = .inStock
|
||||
case -3:
|
||||
self = .transfer
|
||||
case -4:
|
||||
self = .resale
|
||||
default:
|
||||
self = .stars(rawValue)
|
||||
}
|
||||
@ -107,6 +110,8 @@ final class GiftOptionsScreenComponent: Component {
|
||||
return -2
|
||||
case .transfer:
|
||||
return -3
|
||||
case .resale:
|
||||
return -4
|
||||
case let .stars(stars):
|
||||
return stars
|
||||
}
|
||||
@ -136,6 +141,7 @@ final class GiftOptionsScreenComponent: Component {
|
||||
private var starsItems: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
private let tabSelector = ComponentView<Empty>()
|
||||
private var starsFilter: StarsFilter = .all
|
||||
private var switchingFilter = false
|
||||
|
||||
private var _effectiveStarGifts: ([StarGift], StarsFilter)?
|
||||
private var effectiveStarGifts: [StarGift]? {
|
||||
@ -191,6 +197,12 @@ final class GiftOptionsScreenComponent: Component {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case .resale:
|
||||
if case let .generic(gift) = $0 {
|
||||
if let availability = gift.availability, availability.resale > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case .transfer:
|
||||
break
|
||||
}
|
||||
@ -216,6 +228,7 @@ final class GiftOptionsScreenComponent: Component {
|
||||
private(set) weak var state: State?
|
||||
private var environment: EnvironmentType?
|
||||
|
||||
private var tabSelectorOrigin: CGFloat = 0.0
|
||||
private var starsItemsOrigin: CGFloat = 0.0
|
||||
|
||||
private var dismissed = false
|
||||
@ -355,12 +368,28 @@ final class GiftOptionsScreenComponent: Component {
|
||||
|
||||
var ribbon: GiftItemComponent.Ribbon?
|
||||
var isSoldOut = false
|
||||
if case let .generic(gift) = gift {
|
||||
if let _ = gift.soldOut {
|
||||
switch gift {
|
||||
case let .generic(gift):
|
||||
if let availability = gift.availability, availability.resale > 0 {
|
||||
//TODO:localize
|
||||
//TODO:unmock
|
||||
ribbon = GiftItemComponent.Ribbon(
|
||||
text: environment.strings.Gift_Options_Gift_SoldOut,
|
||||
color: .red
|
||||
text: "resale",
|
||||
color: .green
|
||||
)
|
||||
} else if let _ = gift.soldOut {
|
||||
if let availability = gift.availability, availability.resale > 0 {
|
||||
//TODO:localize
|
||||
ribbon = GiftItemComponent.Ribbon(
|
||||
text: "resale",
|
||||
color: .green
|
||||
)
|
||||
} else {
|
||||
ribbon = GiftItemComponent.Ribbon(
|
||||
text: environment.strings.Gift_Options_Gift_SoldOut,
|
||||
color: .red
|
||||
)
|
||||
}
|
||||
isSoldOut = true
|
||||
} else if let _ = gift.availability {
|
||||
ribbon = GiftItemComponent.Ribbon(
|
||||
@ -368,14 +397,31 @@ final class GiftOptionsScreenComponent: Component {
|
||||
color: .blue
|
||||
)
|
||||
}
|
||||
case let .unique(gift):
|
||||
var ribbonColor: GiftItemComponent.Ribbon.Color = .blue
|
||||
for attribute in gift.attributes {
|
||||
if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute {
|
||||
ribbonColor = .custom(outerColor, innerColor)
|
||||
break
|
||||
}
|
||||
}
|
||||
ribbon = GiftItemComponent.Ribbon(
|
||||
text: "#\(gift.number)",
|
||||
font: .monospaced,
|
||||
color: ribbonColor
|
||||
)
|
||||
}
|
||||
|
||||
let subject: GiftItemComponent.Subject
|
||||
switch gift {
|
||||
case let .generic(gift):
|
||||
subject = .starGift(gift: gift, price: "⭐️ \(gift.price)")
|
||||
if let availability = gift.availability, let minResaleStars = availability.minResaleStars {
|
||||
subject = .starGift(gift: gift, price: "⭐️ \(minResaleStars)+")
|
||||
} else {
|
||||
subject = .starGift(gift: gift, price: "⭐️ \(gift.price)")
|
||||
}
|
||||
case let .unique(gift):
|
||||
subject = .uniqueGift(gift: gift)
|
||||
subject = .uniqueGift(gift: gift, price: nil)
|
||||
}
|
||||
|
||||
let _ = visibleItem.update(
|
||||
@ -404,13 +450,27 @@ final class GiftOptionsScreenComponent: Component {
|
||||
mainController = controller
|
||||
}
|
||||
if case let .generic(gift) = gift {
|
||||
if gift.availability?.remains == 0 {
|
||||
let giftController = GiftViewScreen(
|
||||
context: component.context,
|
||||
subject: .soldOutGift(gift)
|
||||
)
|
||||
mainController.push(giftController)
|
||||
} else {
|
||||
var forceStore = !"".isEmpty
|
||||
#if DEBUG
|
||||
forceStore = true
|
||||
#endif
|
||||
|
||||
if let availability = gift.availability, availability.remains == 0 || (availability.resale > 0 && forceStore) {
|
||||
if availability.resale > 0 {
|
||||
let storeController = component.context.sharedContext.makeGiftStoreController(
|
||||
context: component.context,
|
||||
peerId: component.peerId,
|
||||
gift: gift
|
||||
)
|
||||
mainController.push(storeController)
|
||||
} else {
|
||||
let giftController = GiftViewScreen(
|
||||
context: component.context,
|
||||
subject: .soldOutGift(gift)
|
||||
)
|
||||
mainController.push(giftController)
|
||||
}
|
||||
} else {
|
||||
var forceUnique = false
|
||||
if let disallowedGifts = self.state?.disallowedGifts, disallowedGifts.contains(.limited) && !disallowedGifts.contains(.unique) {
|
||||
forceUnique = true
|
||||
@ -475,6 +535,53 @@ final class GiftOptionsScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
var topPanelHeight = environment.navigationHeight
|
||||
let tabSelectorThreshold = self.tabSelectorOrigin - 8.0
|
||||
if contentOffset > tabSelectorThreshold - environment.navigationHeight {
|
||||
topPanelHeight += 39.0
|
||||
}
|
||||
|
||||
if let tabSelectorView = self.tabSelector.view {
|
||||
let tabSelectorSize = tabSelectorView.bounds.size
|
||||
transition.setFrame(view: tabSelectorView, frame: CGRect(origin: CGPoint(x: floor((availableWidth - tabSelectorSize.width) / 2.0), y: max(56.0, self.tabSelectorOrigin - contentOffset)), size: tabSelectorSize))
|
||||
}
|
||||
|
||||
var panelTransition = transition
|
||||
if self.topPanel.view?.superview != nil && !self.switchingFilter {
|
||||
panelTransition = .spring(duration: 0.3)
|
||||
}
|
||||
let topPanelSize = self.topPanel.update(
|
||||
transition: panelTransition,
|
||||
component: AnyComponent(BlurredBackgroundComponent(
|
||||
color: environment.theme.rootController.navigationBar.blurredBackgroundColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableWidth, height: topPanelHeight)
|
||||
)
|
||||
|
||||
let topSeparatorSize = self.topSeparator.update(
|
||||
transition: panelTransition,
|
||||
component: AnyComponent(Rectangle(
|
||||
color: environment.theme.rootController.navigationBar.separatorColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableWidth, height: UIScreenPixel)
|
||||
)
|
||||
let topPanelFrame = CGRect(origin: .zero, size: CGSize(width: availableWidth, height: topPanelSize.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 topPanelView.superview == nil {
|
||||
if let headerView = self.header.view {
|
||||
self.insertSubview(topSeparatorView, aboveSubview: headerView)
|
||||
self.insertSubview(topPanelView, aboveSubview: headerView)
|
||||
}
|
||||
}
|
||||
panelTransition.setFrame(view: topPanelView, frame: topPanelFrame)
|
||||
panelTransition.setFrame(view: topSeparatorView, frame: topSeparatorFrame)
|
||||
}
|
||||
|
||||
let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height)
|
||||
if interactive, bottomContentOffset < 320.0, case .transfer = self.starsFilter {
|
||||
self.state?.starGiftsContext.loadMore()
|
||||
@ -755,34 +862,34 @@ final class GiftOptionsScreenComponent: Component {
|
||||
}
|
||||
transition.setBounds(view: headerView, bounds: CGRect(origin: .zero, size: headerSize))
|
||||
}
|
||||
|
||||
let topPanelSize = self.topPanel.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(BlurredBackgroundComponent(
|
||||
color: theme.rootController.navigationBar.blurredBackgroundColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: environment.navigationHeight)
|
||||
)
|
||||
|
||||
let topSeparatorSize = self.topSeparator.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Rectangle(
|
||||
color: theme.rootController.navigationBar.separatorColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: UIScreenPixel)
|
||||
)
|
||||
let topPanelFrame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: topPanelSize.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 topPanelView.superview == nil {
|
||||
self.addSubview(topPanelView)
|
||||
self.addSubview(topSeparatorView)
|
||||
}
|
||||
transition.setFrame(view: topPanelView, frame: topPanelFrame)
|
||||
transition.setFrame(view: topSeparatorView, frame: topSeparatorFrame)
|
||||
}
|
||||
|
||||
// let topPanelSize = self.topPanel.update(
|
||||
// transition: transition,
|
||||
// component: AnyComponent(BlurredBackgroundComponent(
|
||||
// color: theme.rootController.navigationBar.blurredBackgroundColor
|
||||
// )),
|
||||
// environment: {},
|
||||
// containerSize: CGSize(width: availableSize.width, height: environment.navigationHeight)
|
||||
// )
|
||||
//
|
||||
// let topSeparatorSize = self.topSeparator.update(
|
||||
// transition: transition,
|
||||
// component: AnyComponent(Rectangle(
|
||||
// color: theme.rootController.navigationBar.separatorColor
|
||||
// )),
|
||||
// environment: {},
|
||||
// containerSize: CGSize(width: availableSize.width, height: UIScreenPixel)
|
||||
// )
|
||||
// let topPanelFrame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: topPanelSize.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 topPanelView.superview == nil {
|
||||
// self.addSubview(topPanelView)
|
||||
// self.addSubview(topSeparatorView)
|
||||
// }
|
||||
// transition.setFrame(view: topPanelView, frame: topPanelFrame)
|
||||
// transition.setFrame(view: topSeparatorView, frame: topSeparatorFrame)
|
||||
// }
|
||||
|
||||
let cancelButtonSize = self.cancelButton.update(
|
||||
transition: transition,
|
||||
@ -1186,13 +1293,17 @@ final class GiftOptionsScreenComponent: Component {
|
||||
}
|
||||
|
||||
var hasLimited = false
|
||||
var hasResale = false
|
||||
var starsAmountsSet = Set<Int64>()
|
||||
if let starGifts = self.state?.starGifts {
|
||||
for gift in starGifts {
|
||||
if case let .generic(gift) = gift {
|
||||
starsAmountsSet.insert(gift.price)
|
||||
if gift.availability != nil {
|
||||
if let availability = gift.availability {
|
||||
hasLimited = true
|
||||
if availability.resale > 0 {
|
||||
hasResale = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1210,6 +1321,14 @@ final class GiftOptionsScreenComponent: Component {
|
||||
title: strings.Gift_Options_Gift_Filter_InStock
|
||||
))
|
||||
|
||||
if hasResale {
|
||||
//TODO:localize
|
||||
tabSelectorItems.append(TabSelectorComponent.Item(
|
||||
id: AnyHashable(StarsFilter.resale.rawValue),
|
||||
title: "Resale"
|
||||
))
|
||||
}
|
||||
|
||||
let starsAmounts = Array(starsAmountsSet).sorted()
|
||||
for amount in starsAmounts {
|
||||
tabSelectorItems.append(TabSelectorComponent.Item(
|
||||
@ -1235,17 +1354,26 @@ final class GiftOptionsScreenComponent: Component {
|
||||
}
|
||||
let starsFilter = StarsFilter(rawValue: idValue)
|
||||
if self.starsFilter != starsFilter {
|
||||
if self.scrollView.contentOffset.y > self.tabSelectorOrigin - 56.0 {
|
||||
self.scrollView.setContentOffset(CGPoint(x: 0.0, y: self.tabSelectorOrigin - 56.0), animated: true)
|
||||
}
|
||||
|
||||
self.switchingFilter = true
|
||||
self.starsFilter = starsFilter
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.25))
|
||||
Queue.mainQueue().after(0.1, {
|
||||
self.switchingFilter = false
|
||||
})
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 10.0 * 2.0, height: 50.0)
|
||||
)
|
||||
self.tabSelectorOrigin = contentHeight
|
||||
if let tabSelectorView = self.tabSelector.view {
|
||||
if tabSelectorView.superview == nil {
|
||||
self.scrollView.addSubview(tabSelectorView)
|
||||
self.addSubview(tabSelectorView)
|
||||
}
|
||||
transition.setFrame(view: tabSelectorView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - tabSelectorSize.width) / 2.0), y: contentHeight), size: tabSelectorSize))
|
||||
}
|
||||
|
51
submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD
Normal file
51
submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD
Normal file
@ -0,0 +1,51 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "GiftStoreScreen",
|
||||
module_name = "GiftStoreScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/ItemListUI",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/Components/SheetComponent",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/TelegramUI/Components/ScrollComponent",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
|
||||
"//submodules/Components/BlurredBackgroundComponent",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
|
||||
"//submodules/ConfettiEffect",
|
||||
"//submodules/InAppPurchaseManager",
|
||||
"//submodules/TelegramUI/Components/TabSelectorComponent",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftSetupScreen",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftViewScreen",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,331 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import PlainButtonComponent
|
||||
import MultilineTextWithEntitiesComponent
|
||||
import BundleIconComponent
|
||||
import TextFormat
|
||||
import AccountContext
|
||||
|
||||
public final class FilterSelectorComponent: Component {
|
||||
public struct Colors: Equatable {
|
||||
public var foreground: UIColor
|
||||
public var background: UIColor
|
||||
|
||||
public init(
|
||||
foreground: UIColor,
|
||||
background: UIColor
|
||||
) {
|
||||
self.foreground = foreground
|
||||
self.background = background
|
||||
}
|
||||
}
|
||||
|
||||
public struct Item: Equatable {
|
||||
public var id: AnyHashable
|
||||
public var title: String
|
||||
public var action: (UIView) -> Void
|
||||
|
||||
public init(
|
||||
id: AnyHashable,
|
||||
title: String,
|
||||
action: @escaping (UIView) -> Void
|
||||
) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
return lhs.id == rhs.id && lhs.title == rhs.title
|
||||
}
|
||||
}
|
||||
|
||||
public let context: AccountContext?
|
||||
public let colors: Colors
|
||||
public let items: [Item]
|
||||
|
||||
public init(
|
||||
context: AccountContext? = nil,
|
||||
colors: Colors,
|
||||
items: [Item]
|
||||
) {
|
||||
self.context = context
|
||||
self.colors = colors
|
||||
self.items = items
|
||||
}
|
||||
|
||||
public static func ==(lhs: FilterSelectorComponent, rhs: FilterSelectorComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.colors != rhs.colors {
|
||||
return false
|
||||
}
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private final class VisibleItem {
|
||||
let title = ComponentView<Empty>()
|
||||
|
||||
init() {
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIScrollView {
|
||||
private var component: FilterSelectorComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private var visibleItems: [AnyHashable: VisibleItem] = [:]
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.showsVerticalScrollIndicator = false
|
||||
self.showsHorizontalScrollIndicator = false
|
||||
self.scrollsToTop = false
|
||||
self.delaysContentTouches = false
|
||||
self.canCancelContentTouches = true
|
||||
self.contentInsetAdjustmentBehavior = .never
|
||||
self.alwaysBounceVertical = false
|
||||
self.clipsToBounds = false
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
override public func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func update(component: FilterSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let baseHeight: CGFloat = 28.0
|
||||
|
||||
var spacing: CGFloat = 6.0
|
||||
|
||||
let itemFont = Font.semibold(14.0)
|
||||
let allowScroll = true
|
||||
|
||||
var innerContentWidth: CGFloat = 0.0
|
||||
|
||||
var validIds: [AnyHashable] = []
|
||||
var index = 0
|
||||
var itemViews: [AnyHashable: (VisibleItem, CGSize, ComponentTransition)] = [:]
|
||||
|
||||
for item in component.items {
|
||||
var itemTransition = transition
|
||||
let itemView: VisibleItem
|
||||
if let current = self.visibleItems[item.id] {
|
||||
itemView = current
|
||||
} else {
|
||||
itemView = VisibleItem()
|
||||
self.visibleItems[item.id] = itemView
|
||||
itemTransition = itemTransition.withAnimation(.none)
|
||||
}
|
||||
|
||||
let itemId = item.id
|
||||
validIds.append(itemId)
|
||||
|
||||
let itemSize = itemView.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(ItemComponent(
|
||||
context: component.context,
|
||||
text: item.title,
|
||||
font: itemFont,
|
||||
color: component.colors.foreground,
|
||||
backgroundColor: component.colors.background
|
||||
)),
|
||||
effectAlignment: .center,
|
||||
minSize: nil,
|
||||
action: { [weak itemView] in
|
||||
if let view = itemView?.title.view {
|
||||
item.action(view)
|
||||
}
|
||||
},
|
||||
animateScale: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 200.0, height: 100.0)
|
||||
)
|
||||
innerContentWidth += itemSize.width
|
||||
itemViews[item.id] = (itemView, itemSize, itemTransition)
|
||||
index += 1
|
||||
}
|
||||
|
||||
let estimatedContentWidth = 2.0 * spacing + innerContentWidth + (CGFloat(component.items.count - 1) * spacing)
|
||||
if estimatedContentWidth > availableSize.width && !allowScroll {
|
||||
spacing = (availableSize.width - innerContentWidth) / CGFloat(component.items.count + 1)
|
||||
}
|
||||
|
||||
var contentWidth: CGFloat = spacing
|
||||
for item in component.items {
|
||||
guard let (itemView, itemSize, itemTransition) = itemViews[item.id] else {
|
||||
continue
|
||||
}
|
||||
if contentWidth > spacing {
|
||||
contentWidth += spacing
|
||||
}
|
||||
let itemFrame = CGRect(origin: CGPoint(x: contentWidth, y: floor((baseHeight - itemSize.height) * 0.5)), size: itemSize)
|
||||
contentWidth = itemFrame.maxX
|
||||
|
||||
if let itemTitleView = itemView.title.view {
|
||||
if itemTitleView.superview == nil {
|
||||
itemTitleView.layer.anchorPoint = CGPoint()
|
||||
self.addSubview(itemTitleView)
|
||||
}
|
||||
itemTransition.setPosition(view: itemTitleView, position: itemFrame.origin)
|
||||
itemTransition.setBounds(view: itemTitleView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
|
||||
}
|
||||
}
|
||||
contentWidth += spacing
|
||||
|
||||
var removeIds: [AnyHashable] = []
|
||||
for (id, itemView) in self.visibleItems {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
itemView.title.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.visibleItems.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
self.contentSize = CGSize(width: contentWidth, height: baseHeight)
|
||||
self.disablesInteractiveTransitionGestureRecognizer = contentWidth > availableSize.width
|
||||
|
||||
return CGSize(width: min(contentWidth, availableSize.width), height: baseHeight)
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public 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)
|
||||
}
|
||||
}
|
||||
|
||||
extension CGRect {
|
||||
func interpolate(with other: CGRect, fraction: CGFloat) -> CGRect {
|
||||
return CGRect(
|
||||
x: self.origin.x * (1.0 - fraction) + (other.origin.x) * fraction,
|
||||
y: self.origin.y * (1.0 - fraction) + (other.origin.y) * fraction,
|
||||
width: self.size.width * (1.0 - fraction) + (other.size.width) * fraction,
|
||||
height: self.size.height * (1.0 - fraction) + (other.size.height) * fraction
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private final class ItemComponent: CombinedComponent {
|
||||
let context: AccountContext?
|
||||
let text: String
|
||||
let font: UIFont
|
||||
let color: UIColor
|
||||
let backgroundColor: UIColor
|
||||
|
||||
init(
|
||||
context: AccountContext?,
|
||||
text: String,
|
||||
font: UIFont,
|
||||
color: UIColor,
|
||||
backgroundColor: UIColor
|
||||
) {
|
||||
self.context = context
|
||||
self.text = text
|
||||
self.font = font
|
||||
self.color = color
|
||||
self.backgroundColor = backgroundColor
|
||||
}
|
||||
|
||||
static func ==(lhs: ItemComponent, rhs: ItemComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if lhs.font != rhs.font {
|
||||
return false
|
||||
}
|
||||
if lhs.color != rhs.color {
|
||||
return false
|
||||
}
|
||||
if lhs.backgroundColor != rhs.backgroundColor {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let background = Child(RoundedRectangle.self)
|
||||
let title = Child(MultilineTextWithEntitiesComponent.self)
|
||||
let icon = Child(BundleIconComponent.self)
|
||||
|
||||
return { context in
|
||||
let component = context.component
|
||||
|
||||
let attributedTitle = NSMutableAttributedString(string: component.text, font: component.font, textColor: component.color)
|
||||
let range = (attributedTitle.string as NSString).range(of: "⭐️")
|
||||
if range.location != NSNotFound {
|
||||
attributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
|
||||
}
|
||||
|
||||
let title = title.update(
|
||||
component: MultilineTextWithEntitiesComponent(
|
||||
context: component.context,
|
||||
animationCache: component.context?.animationCache,
|
||||
animationRenderer: component.context?.animationRenderer,
|
||||
placeholderColor: .white,
|
||||
text: .plain(attributedTitle)
|
||||
),
|
||||
availableSize: context.availableSize,
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
let icon = icon.update(
|
||||
component: BundleIconComponent(
|
||||
name: "Item List/ExpandableSelectorArrows",
|
||||
tintColor: component.color
|
||||
),
|
||||
availableSize: CGSize(width: 100, height: 100),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
let padding: CGFloat = 12.0
|
||||
let spacing: CGFloat = 4.0
|
||||
let totalWidth = title.size.width + icon.size.width + spacing
|
||||
let size = CGSize(width: totalWidth + padding * 2.0, height: 28.0)
|
||||
let background = background.update(
|
||||
component: RoundedRectangle(
|
||||
color: component.backgroundColor,
|
||||
cornerRadius: 14.0
|
||||
),
|
||||
availableSize: size,
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(background
|
||||
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0))
|
||||
)
|
||||
context.add(title
|
||||
.position(CGPoint(x: padding + title.size.width / 2.0, y: size.height / 2.0))
|
||||
)
|
||||
context.add(icon
|
||||
.position(CGPoint(x: size.width - padding - icon.size.width / 2.0, y: size.height / 2.0))
|
||||
)
|
||||
return size
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,185 @@
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
|
||||
private final class SearchShimmerEffectNode: ASDisplayNode {
|
||||
private var currentBackgroundColor: UIColor?
|
||||
private var currentForegroundColor: UIColor?
|
||||
private let imageNodeContainer: ASDisplayNode
|
||||
private let imageNode: ASImageNode
|
||||
|
||||
private var absoluteLocation: (CGRect, CGSize)?
|
||||
private var isCurrentlyInHierarchy = false
|
||||
private var shouldBeAnimating = false
|
||||
|
||||
override init() {
|
||||
self.imageNodeContainer = ASDisplayNode()
|
||||
self.imageNodeContainer.isLayerBacked = true
|
||||
|
||||
self.imageNode = ASImageNode()
|
||||
self.imageNode.isLayerBacked = true
|
||||
self.imageNode.displaysAsynchronously = false
|
||||
self.imageNode.displayWithoutProcessing = true
|
||||
self.imageNode.contentMode = .scaleToFill
|
||||
|
||||
super.init()
|
||||
|
||||
self.isLayerBacked = true
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.imageNodeContainer.addSubnode(self.imageNode)
|
||||
self.addSubnode(self.imageNodeContainer)
|
||||
}
|
||||
|
||||
override func didEnterHierarchy() {
|
||||
super.didEnterHierarchy()
|
||||
|
||||
self.isCurrentlyInHierarchy = true
|
||||
self.updateAnimation()
|
||||
}
|
||||
|
||||
override func didExitHierarchy() {
|
||||
super.didExitHierarchy()
|
||||
|
||||
self.isCurrentlyInHierarchy = false
|
||||
self.updateAnimation()
|
||||
}
|
||||
|
||||
func update(backgroundColor: UIColor, foregroundColor: UIColor) {
|
||||
if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.argb == backgroundColor.argb, let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.argb == foregroundColor.argb {
|
||||
return
|
||||
}
|
||||
self.currentBackgroundColor = backgroundColor
|
||||
self.currentForegroundColor = foregroundColor
|
||||
|
||||
self.imageNode.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.clip(to: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor
|
||||
let peakColor = foregroundColor.cgColor
|
||||
|
||||
var locations: [CGFloat] = [0.0, 0.5, 1.0]
|
||||
let colors: [CGColor] = [transparentColor, peakColor, transparentColor]
|
||||
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
})
|
||||
}
|
||||
|
||||
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
|
||||
if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize {
|
||||
return
|
||||
}
|
||||
let sizeUpdated = self.absoluteLocation?.1 != containerSize
|
||||
let frameUpdated = self.absoluteLocation?.0 != rect
|
||||
self.absoluteLocation = (rect, containerSize)
|
||||
|
||||
if sizeUpdated {
|
||||
if self.shouldBeAnimating {
|
||||
self.imageNode.layer.removeAnimation(forKey: "shimmer")
|
||||
self.addImageAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
if frameUpdated {
|
||||
self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize)
|
||||
}
|
||||
|
||||
self.updateAnimation()
|
||||
}
|
||||
|
||||
private func updateAnimation() {
|
||||
let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil
|
||||
if shouldBeAnimating != self.shouldBeAnimating {
|
||||
self.shouldBeAnimating = shouldBeAnimating
|
||||
if shouldBeAnimating {
|
||||
self.addImageAnimation()
|
||||
} else {
|
||||
self.imageNode.layer.removeAnimation(forKey: "shimmer")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addImageAnimation() {
|
||||
guard let containerSize = self.absoluteLocation?.1 else {
|
||||
return
|
||||
}
|
||||
let gradientHeight: CGFloat = 250.0
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight))
|
||||
let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
|
||||
animation.repeatCount = Float.infinity
|
||||
animation.beginTime = 1.0
|
||||
self.imageNode.layer.add(animation, forKey: "shimmer")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class LoadingShimmerNode: ASDisplayNode {
|
||||
private let backgroundColorNode: ASDisplayNode
|
||||
private let effectNode: SearchShimmerEffectNode
|
||||
private let maskNode: ASImageNode
|
||||
private var currentParams: (size: CGSize, theme: PresentationTheme)?
|
||||
|
||||
override init() {
|
||||
self.backgroundColorNode = ASDisplayNode()
|
||||
self.effectNode = SearchShimmerEffectNode()
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.isUserInteractionEnabled = false
|
||||
|
||||
self.addSubnode(self.backgroundColorNode)
|
||||
self.addSubnode(self.effectNode)
|
||||
self.addSubnode(self.maskNode)
|
||||
}
|
||||
|
||||
func update(size: CGSize, theme: PresentationTheme, transition: ContainedViewLayoutTransition) {
|
||||
let color = theme.list.itemSecondaryTextColor.mixedWith(theme.list.blocksBackgroundColor, alpha: 0.85)
|
||||
|
||||
if self.currentParams?.size != size || self.currentParams?.theme !== theme {
|
||||
self.currentParams = (size, theme)
|
||||
|
||||
self.backgroundColorNode.backgroundColor = color
|
||||
|
||||
self.maskNode.image = generateImage(size, rotatedContext: { size, context in
|
||||
context.setFillColor(theme.list.blocksBackgroundColor.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
var currentY: CGFloat = 0.0
|
||||
var rowIndex: Int = 0
|
||||
|
||||
let sideInset: CGFloat = 16.0// + environment.safeInsets.left
|
||||
let optionSpacing: CGFloat = 10.0
|
||||
let optionWidth = (size.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
|
||||
let itemSize = CGSize(width: optionWidth, height: 154.0)
|
||||
|
||||
context.setBlendMode(.copy)
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
|
||||
while currentY < size.height {
|
||||
for i in 0 ..< 3 {
|
||||
let itemOrigin = CGPoint(x: sideInset + CGFloat(i) * (itemSize.width + optionSpacing), y: 2.0 + CGFloat(rowIndex) * (itemSize.height + optionSpacing))
|
||||
context.addPath(CGPath(roundedRect: CGRect(origin: itemOrigin, size: itemSize), cornerWidth: 10.0, cornerHeight: 10.0, transform: nil))
|
||||
}
|
||||
currentY += itemSize.height
|
||||
rowIndex += 1
|
||||
}
|
||||
context.fillPath()
|
||||
})
|
||||
|
||||
self.effectNode.update(backgroundColor: color, foregroundColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4))
|
||||
self.effectNode.updateAbsoluteRect(CGRect(origin: CGPoint(), size: size), within: size)
|
||||
}
|
||||
transition.updateFrame(node: self.backgroundColorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
|
||||
transition.updateFrame(node: self.maskNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
|
||||
transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
|
||||
}
|
||||
}
|
@ -164,7 +164,7 @@ private final class GiftTransferAlertContentNode: AlertContentNode {
|
||||
theme: self.presentationTheme,
|
||||
strings: self.strings,
|
||||
peer: nil,
|
||||
subject: .uniqueGift(gift: self.gift),
|
||||
subject: .uniqueGift(gift: self.gift, price: nil),
|
||||
mode: .thumbnail
|
||||
)
|
||||
),
|
||||
|
@ -159,7 +159,7 @@ private final class SheetContent: CombinedComponent {
|
||||
|
||||
var ribbonColor: GiftItemComponent.Ribbon.Color = .blue
|
||||
for attribute in displayGift.attributes {
|
||||
if case let .backdrop(_, innerColor, outerColor, _, _, _) = attribute {
|
||||
if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute {
|
||||
ribbonColor = .custom(outerColor, innerColor)
|
||||
break
|
||||
}
|
||||
@ -175,7 +175,7 @@ private final class SheetContent: CombinedComponent {
|
||||
context: component.context,
|
||||
theme: theme,
|
||||
strings: strings,
|
||||
subject: .uniqueGift(gift: displayGift),
|
||||
subject: .uniqueGift(gift: displayGift, price: nil),
|
||||
ribbon: GiftItemComponent.Ribbon(text: "#\(displayGift.number)", font: .monospaced, color: ribbonColor),
|
||||
mode: .grid
|
||||
)
|
||||
|
@ -57,6 +57,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
let transferGift: () -> Void
|
||||
let upgradeGift: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)
|
||||
let shareGift: () -> Void
|
||||
let resellGift: () -> Void
|
||||
let showAttributeInfo: (Any, String) -> Void
|
||||
let viewUpgraded: (EngineMessage.Id) -> Void
|
||||
let openMore: (ASDisplayNode, ContextGesture?) -> Void
|
||||
@ -77,6 +78,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
transferGift: @escaping () -> Void,
|
||||
upgradeGift: @escaping ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>),
|
||||
shareGift: @escaping () -> Void,
|
||||
resellGift: @escaping () -> Void,
|
||||
showAttributeInfo: @escaping (Any, String) -> Void,
|
||||
viewUpgraded: @escaping (EngineMessage.Id) -> Void,
|
||||
openMore: @escaping (ASDisplayNode, ContextGesture?) -> Void,
|
||||
@ -96,6 +98,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
self.transferGift = transferGift
|
||||
self.upgradeGift = upgradeGift
|
||||
self.shareGift = shareGift
|
||||
self.resellGift = resellGift
|
||||
self.showAttributeInfo = showAttributeInfo
|
||||
self.viewUpgraded = viewUpgraded
|
||||
self.openMore = openMore
|
||||
@ -126,6 +129,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
|
||||
var cachedCircleImage: UIImage?
|
||||
var cachedStarImage: (UIImage, PresentationTheme)?
|
||||
var cachedSmallStarImage: (UIImage, PresentationTheme)?
|
||||
|
||||
var cachedChevronImage: (UIImage, PresentationTheme)?
|
||||
var cachedSmallChevronImage: (UIImage, PresentationTheme)?
|
||||
@ -138,6 +142,10 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
var upgradeDisposable: Disposable?
|
||||
let levelsDisposable = MetaDisposable()
|
||||
|
||||
var buyForm: BotPaymentForm?
|
||||
var buyFormDisposable: Disposable?
|
||||
var buyDisposable: Disposable?
|
||||
|
||||
var inWearPreview = false
|
||||
var pendingWear = false
|
||||
var pendingTakeOff = false
|
||||
@ -192,6 +200,17 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let _ = arguments.resellStars {
|
||||
self.buyFormDisposable = (context.engine.payments.fetchBotPaymentForm(source: .starGiftResale(slug: gift.slug, toPeerId: context.account.peerId), themeParams: nil)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] paymentForm in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.buyForm = paymentForm
|
||||
self.updated()
|
||||
})
|
||||
}
|
||||
} else if case let .generic(gift) = arguments.gift {
|
||||
if arguments.canUpgrade || arguments.upgradeStars != nil {
|
||||
self.sampleDisposable.add((context.engine.payments.starGiftUpgradePreview(giftId: gift.id)
|
||||
@ -266,6 +285,13 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
self.optionsPromise.set(context.engine.payments.starsTopUpOptions()
|
||||
|> map(Optional.init))
|
||||
}
|
||||
|
||||
if let controller = getController() as? GiftViewScreen {
|
||||
controller.updateSubject.connect { [weak self] subject in
|
||||
self?.subject = subject
|
||||
self?.updated()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -273,6 +299,8 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
self.sampleDisposable.dispose()
|
||||
self.upgradeFormDisposable?.dispose()
|
||||
self.upgradeDisposable?.dispose()
|
||||
self.buyFormDisposable?.dispose()
|
||||
self.buyDisposable?.dispose()
|
||||
self.levelsDisposable.dispose()
|
||||
}
|
||||
|
||||
@ -331,6 +359,99 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
let action = {
|
||||
let proceed: (Int64) -> Void = { formId in
|
||||
self.inProgress = true
|
||||
self.updated()
|
||||
|
||||
let signal = self.context.engine.payments.sendStarsPaymentForm(formId: formId, source: .starGiftResale(slug: uniqueGift.slug, toPeerId: self.context.account.peerId))
|
||||
|> mapError { _ -> SendBotPaymentFormError in
|
||||
return .generic
|
||||
}
|
||||
|> mapToSignal { result in
|
||||
if case let .done(_, _, gift) = result, let gift {
|
||||
return .single(gift)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|
||||
self.buyDisposable = (signal
|
||||
|> deliverOnMainQueue).start(next: { [weak self, weak starsContext] result in
|
||||
guard let self, let controller = self.getController() as? GiftViewScreen else {
|
||||
return
|
||||
}
|
||||
self.inProgress = false
|
||||
|
||||
controller.animateSuccess()
|
||||
self.updated(transition: .spring(duration: 0.4))
|
||||
|
||||
Queue.mainQueue().after(0.5) {
|
||||
starsContext?.load(force: true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if let buyForm = self.buyForm, let price = buyForm.invoice.prices.first?.amount {
|
||||
if starsState.balance < StarsAmount(value: price, nanos: 0) {
|
||||
let _ = (self.optionsPromise.get()
|
||||
|> filter { $0 != nil }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] options in
|
||||
guard let self, let controller = self.getController() else {
|
||||
return
|
||||
}
|
||||
let purchaseController = self.context.sharedContext.makeStarsPurchaseScreen(
|
||||
context: self.context,
|
||||
starsContext: starsContext,
|
||||
options: options ?? [],
|
||||
purpose: .upgradeStarGift(requiredStars: price),
|
||||
completion: { [weak self, weak starsContext] stars in
|
||||
guard let self, let starsContext else {
|
||||
return
|
||||
}
|
||||
self.inProgress = true
|
||||
self.updated()
|
||||
|
||||
starsContext.add(balance: StarsAmount(value: stars, nanos: 0))
|
||||
let _ = (starsContext.onUpdate
|
||||
|> deliverOnMainQueue).start(next: {
|
||||
proceed(buyForm.id)
|
||||
})
|
||||
}
|
||||
)
|
||||
controller.push(purchaseController)
|
||||
})
|
||||
} else {
|
||||
proceed(buyForm.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
let giftTitle = "\(uniqueGift.title) #\(uniqueGift.number)"
|
||||
let alertController = textAlertController(
|
||||
context: self.context,
|
||||
title: "Confirm Payment",
|
||||
text: "Do you really want to buy **\(giftTitle)** for **\(arguments.resellStars ?? 0)** Stars?",
|
||||
actions: [
|
||||
TextAlertAction(type: .defaultAction, title: "Buy for \(arguments.resellStars ?? 0) Stars", action: {
|
||||
action()
|
||||
}),
|
||||
TextAlertAction(type: .genericAction, title: "Cancel", action: {
|
||||
})
|
||||
],
|
||||
actionLayout: .vertical,
|
||||
parseMarkdown: true
|
||||
)
|
||||
if let controller = self.getController() as? GiftViewScreen {
|
||||
controller.present(alertController, in: .window(.root))
|
||||
}
|
||||
}
|
||||
|
||||
func commitUpgrade() {
|
||||
guard let arguments = self.subject.arguments, let peerId = arguments.peerId, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else {
|
||||
return
|
||||
@ -352,8 +473,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
self.inProgress = false
|
||||
self.inUpgradePreview = false
|
||||
|
||||
self.subject = .profileGift(peerId, result)
|
||||
controller.subject = self.subject
|
||||
controller.subject = .profileGift(peerId, result)
|
||||
controller.animateSuccess()
|
||||
self.updated(transition: .spring(duration: 0.4))
|
||||
|
||||
@ -418,7 +538,8 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
|
||||
let transferButton = Child(PlainButtonComponent.self)
|
||||
let wearButton = Child(PlainButtonComponent.self)
|
||||
let shareButton = Child(PlainButtonComponent.self)
|
||||
// let shareButton = Child(PlainButtonComponent.self)
|
||||
let resellButton = Child(PlainButtonComponent.self)
|
||||
|
||||
let wearAvatar = Child(AvatarComponent.self)
|
||||
let wearPeerName = Child(MultilineTextComponent.self)
|
||||
@ -1027,7 +1148,12 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
var descriptionText: String
|
||||
if let uniqueGift {
|
||||
titleString = uniqueGift.title
|
||||
descriptionText = "\(strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))"
|
||||
|
||||
if let resellPrice = uniqueGift.resellStars, incoming {
|
||||
descriptionText = "\(strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator)) • Listed for * \(resellPrice)"
|
||||
} else {
|
||||
descriptionText = "\(strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))"
|
||||
}
|
||||
} else if soldOut {
|
||||
descriptionText = strings.Gift_View_UnavailableDescription
|
||||
} else if upgraded {
|
||||
@ -1095,6 +1221,9 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
|
||||
if !descriptionText.isEmpty {
|
||||
let linkColor = theme.actionSheet.controlAccentColor
|
||||
if state.cachedSmallStarImage == nil || state.cachedSmallStarImage?.1 !== environment.theme {
|
||||
state.cachedSmallStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Premium/Stars/ButtonStar"), color: .white)!, theme)
|
||||
}
|
||||
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme {
|
||||
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme)
|
||||
}
|
||||
@ -1114,6 +1243,11 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
|
||||
descriptionText = descriptionText.replacingOccurrences(of: " >]", with: "\u{00A0}>]")
|
||||
let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
|
||||
if let range = attributedString.string.range(of: "*"), let starImage = state.cachedSmallStarImage?.0 {
|
||||
attributedString.addAttribute(.font, value: Font.regular(13.0), range: NSRange(range, in: attributedString.string))
|
||||
attributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: attributedString.string))
|
||||
attributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedString.string))
|
||||
}
|
||||
if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 {
|
||||
attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string))
|
||||
}
|
||||
@ -1221,7 +1355,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
if case let .model(_, file, _) = attribute {
|
||||
fileId = file.fileId.id
|
||||
}
|
||||
if case let .backdrop(_, innerColor, _, _, _, _) = attribute {
|
||||
if case let .backdrop(_, _, innerColor, _, _, _, _) = attribute {
|
||||
color = UIColor(rgb: UInt32(bitPattern: innerColor))
|
||||
}
|
||||
}
|
||||
@ -1534,28 +1668,52 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
)
|
||||
buttonOriginX += buttonWidth + buttonSpacing
|
||||
|
||||
let shareButton = shareButton.update(
|
||||
//TODO:localize
|
||||
let resellButton = resellButton.update(
|
||||
component: PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
HeaderButtonComponent(
|
||||
title: strings.Gift_View_Header_Share,
|
||||
iconName: "Premium/Collectible/Share"
|
||||
title: uniqueGift.resellStars == nil ? "sell" : "unlist",
|
||||
iconName: uniqueGift.resellStars == nil ? "Premium/Collectible/Sell" : "Premium/Collectible/Unlist"
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: {
|
||||
component.shareGift()
|
||||
component.resellGift()
|
||||
}
|
||||
),
|
||||
environment: {},
|
||||
availableSize: CGSize(width: buttonWidth, height: buttonHeight),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(shareButton
|
||||
context.add(resellButton
|
||||
.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 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
|
||||
@ -1586,7 +1744,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
|
||||
percentage = Float(rarity) * 0.1
|
||||
tag = modelButtonTag
|
||||
case let .backdrop(name, _, _, _, _, rarity):
|
||||
case let .backdrop(name, _, _, _, _, _, rarity):
|
||||
id = "backdrop"
|
||||
title = strings.Gift_Unique_Backdrop
|
||||
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
|
||||
@ -1907,14 +2065,27 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
originY += table.size.height + 23.0
|
||||
}
|
||||
|
||||
if ((incoming && !converted && !upgraded) || exported) && (!showUpgradePreview && !showWearPreview) {
|
||||
var resellStars: Int64?
|
||||
if let uniqueGift {
|
||||
resellStars = uniqueGift.resellStars
|
||||
}
|
||||
if ((incoming && !converted && !upgraded) || exported || (!incoming && resellStars != nil)) && (!showUpgradePreview && !showWearPreview) {
|
||||
let linkColor = theme.actionSheet.controlAccentColor
|
||||
if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme {
|
||||
state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme)
|
||||
}
|
||||
var addressToOpen: String?
|
||||
var descriptionText: String
|
||||
if let uniqueGift, let address = uniqueGift.giftAddress, case .address = uniqueGift.owner {
|
||||
if let uniqueGift, !incoming {
|
||||
//TODO:localize
|
||||
let ownerName: String
|
||||
if case let .peerId(peerId) = uniqueGift.owner {
|
||||
ownerName = state.peerMap[peerId]?.compactDisplayTitle ?? ""
|
||||
} else {
|
||||
ownerName = ""
|
||||
}
|
||||
descriptionText = "\(ownerName) is selling this gift and you can buy it."
|
||||
} else if let uniqueGift, let address = uniqueGift.giftAddress, case .address = uniqueGift.owner {
|
||||
addressToOpen = address
|
||||
descriptionText = strings.Gift_View_TonGiftAddressInfo
|
||||
} else if savedToProfile {
|
||||
@ -2114,14 +2285,15 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
}
|
||||
var upgradeString = strings.Gift_Upgrade_Upgrade
|
||||
if let upgradeForm = state.upgradeForm, let price = upgradeForm.invoice.prices.first?.amount {
|
||||
upgradeString += " # \(presentationStringsFormattedNumber(Int32(price), environment.dateTimeFormat.groupingSeparator))"
|
||||
upgradeString += " # \(presentationStringsFormattedNumber(Int32(price), environment.dateTimeFormat.groupingSeparator))"
|
||||
}
|
||||
let buttonTitle = subject.arguments?.upgradeStars != nil ? strings.Gift_Upgrade_Confirm : upgradeString
|
||||
let buttonAttributedString = NSMutableAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
|
||||
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: environment.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string))
|
||||
buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string))
|
||||
buttonAttributedString.addAttribute(.foregroundColor, value: theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string))
|
||||
buttonAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: buttonAttributedString.string))
|
||||
buttonAttributedString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: buttonAttributedString.string))
|
||||
}
|
||||
buttonChild = button.update(
|
||||
component: ButtonComponent(
|
||||
@ -2205,6 +2377,36 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
availableSize: buttonSize,
|
||||
transition: context.transition
|
||||
)
|
||||
} else if !incoming, let resellStars {
|
||||
if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme {
|
||||
state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme)
|
||||
}
|
||||
var upgradeString = "Buy for"
|
||||
upgradeString += " # \(presentationStringsFormattedNumber(Int32(resellStars), environment.dateTimeFormat.groupingSeparator))"
|
||||
|
||||
let buttonTitle = subject.arguments?.upgradeStars != nil ? strings.Gift_Upgrade_Confirm : upgradeString
|
||||
let buttonAttributedString = NSMutableAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
|
||||
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: theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string))
|
||||
buttonAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: buttonAttributedString.string))
|
||||
buttonAttributedString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: buttonAttributedString.string))
|
||||
}
|
||||
buttonChild = button.update(
|
||||
component: ButtonComponent(
|
||||
background: buttonBackground,
|
||||
content: AnyComponentWithIdentity(
|
||||
id: AnyHashable("buy"),
|
||||
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
|
||||
),
|
||||
isEnabled: true,
|
||||
displaysProgress: state.inProgress,
|
||||
action: { [weak state] in
|
||||
state?.commitBuy()
|
||||
}),
|
||||
availableSize: buttonSize,
|
||||
transition: context.transition
|
||||
)
|
||||
} else {
|
||||
buttonChild = button.update(
|
||||
component: ButtonComponent(
|
||||
@ -2256,6 +2458,7 @@ private final class GiftViewSheetComponent: CombinedComponent {
|
||||
let transferGift: () -> Void
|
||||
let upgradeGift: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)
|
||||
let shareGift: () -> Void
|
||||
let resellGift: () -> Void
|
||||
let viewUpgraded: (EngineMessage.Id) -> Void
|
||||
let openMore: (ASDisplayNode, ContextGesture?) -> Void
|
||||
let showAttributeInfo: (Any, String) -> Void
|
||||
@ -2274,6 +2477,7 @@ private final class GiftViewSheetComponent: CombinedComponent {
|
||||
transferGift: @escaping () -> Void,
|
||||
upgradeGift: @escaping ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>),
|
||||
shareGift: @escaping () -> Void,
|
||||
resellGift: @escaping () -> Void,
|
||||
viewUpgraded: @escaping (EngineMessage.Id) -> Void,
|
||||
openMore: @escaping (ASDisplayNode, ContextGesture?) -> Void,
|
||||
showAttributeInfo: @escaping (Any, String) -> Void
|
||||
@ -2291,6 +2495,7 @@ private final class GiftViewSheetComponent: CombinedComponent {
|
||||
self.transferGift = transferGift
|
||||
self.upgradeGift = upgradeGift
|
||||
self.shareGift = shareGift
|
||||
self.resellGift = resellGift
|
||||
self.viewUpgraded = viewUpgraded
|
||||
self.openMore = openMore
|
||||
self.showAttributeInfo = showAttributeInfo
|
||||
@ -2324,10 +2529,7 @@ private final class GiftViewSheetComponent: CombinedComponent {
|
||||
cancel: { animate in
|
||||
if animate {
|
||||
if let controller = controller() as? GiftViewScreen {
|
||||
controller.dismissAllTooltips()
|
||||
animateOut.invoke(Action { [weak controller] _ in
|
||||
controller?.dismiss(completion: nil)
|
||||
})
|
||||
controller.dismissAnimated()
|
||||
}
|
||||
} else if let controller = controller() {
|
||||
controller.dismiss(animated: false, completion: nil)
|
||||
@ -2344,6 +2546,7 @@ private final class GiftViewSheetComponent: CombinedComponent {
|
||||
transferGift: context.component.transferGift,
|
||||
upgradeGift: context.component.upgradeGift,
|
||||
shareGift: context.component.shareGift,
|
||||
resellGift: context.component.resellGift,
|
||||
showAttributeInfo: context.component.showAttributeInfo,
|
||||
viewUpgraded: context.component.viewUpgraded,
|
||||
openMore: context.component.openMore,
|
||||
@ -2422,20 +2625,20 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
case upgradePreview([StarGift.UniqueGift.Attribute], String)
|
||||
case wearPreview(StarGift.UniqueGift)
|
||||
|
||||
var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, canExportDate: Int32?, upgradeMessageId: Int32?)? {
|
||||
var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellStars: Int64?, canExportDate: Int32?, upgradeMessageId: Int32?)? {
|
||||
switch self {
|
||||
case let .message(message):
|
||||
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction {
|
||||
switch action.action {
|
||||
case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, _, upgradeMessageId, peerId, senderId, savedId):
|
||||
case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, upgradeMessageId, peerId, senderId, savedId):
|
||||
var reference: StarGiftReference
|
||||
if let peerId, let savedId {
|
||||
reference = .peer(peerId: peerId, id: savedId)
|
||||
} else {
|
||||
reference = .message(messageId: message.id)
|
||||
}
|
||||
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, canUpgrade, upgradeStars, nil, nil, upgradeMessageId)
|
||||
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, peerId, senderId, savedId):
|
||||
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId)
|
||||
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, peerId, senderId, savedId, _):
|
||||
var reference: StarGiftReference
|
||||
if let peerId, let savedId {
|
||||
reference = .peer(peerId: peerId, id: savedId)
|
||||
@ -2454,19 +2657,28 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
} else {
|
||||
incoming = message.flags.contains(.Incoming)
|
||||
}
|
||||
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, nil, transferStars, canExportDate, nil)
|
||||
|
||||
var resellStars: Int64?
|
||||
if case let .unique(uniqueGift) = gift {
|
||||
resellStars = uniqueGift.resellStars
|
||||
}
|
||||
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellStars, canExportDate, nil)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
case let .uniqueGift(gift), let .wearPreview(gift):
|
||||
return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, 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, nil, nil, nil)
|
||||
case let .profileGift(peerId, gift):
|
||||
var messageId: EngineMessage.Id?
|
||||
if case let .message(messageIdValue) = gift.reference {
|
||||
messageId = messageIdValue
|
||||
}
|
||||
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, gift.canExportDate, nil)
|
||||
var resellStars: Int64?
|
||||
if case let .unique(uniqueGift) = gift.gift {
|
||||
resellStars = uniqueGift.resellStars
|
||||
}
|
||||
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellStars, gift.canExportDate, nil)
|
||||
case .soldOutGift:
|
||||
return nil
|
||||
case .upgradePreview:
|
||||
@ -2477,7 +2689,13 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
fileprivate var subject: GiftViewScreen.Subject
|
||||
fileprivate var subject: GiftViewScreen.Subject {
|
||||
didSet {
|
||||
self.updateSubject.invoke(self.subject)
|
||||
}
|
||||
}
|
||||
let updateSubject = ActionSlot<GiftViewScreen.Subject>()
|
||||
|
||||
public var disposed: () -> Void = {}
|
||||
|
||||
fileprivate var showBalance = false {
|
||||
@ -2497,6 +2715,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
convertToStars: (() -> Void)? = nil,
|
||||
transferGift: ((Bool, EnginePeer.Id) -> Signal<Never, TransferStarGiftError>)? = nil,
|
||||
upgradeGift: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)? = nil,
|
||||
updateResellStars: ((Int64?) -> Void)? = nil,
|
||||
togglePinnedToTop: ((Bool) -> Bool)? = nil,
|
||||
shareStory: ((StarGift.UniqueGift) -> Void)? = nil
|
||||
) {
|
||||
@ -2514,10 +2733,11 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
var transferGiftImpl: (() -> Void)?
|
||||
var upgradeGiftImpl: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)?
|
||||
var shareGiftImpl: (() -> Void)?
|
||||
var resellGiftImpl: (() -> Void)?
|
||||
var openMoreImpl: ((ASDisplayNode, ContextGesture?) -> Void)?
|
||||
var showAttributeInfoImpl: ((Any, String) -> Void)?
|
||||
var viewUpgradedImpl: ((EngineMessage.Id) -> Void)?
|
||||
|
||||
|
||||
super.init(
|
||||
context: context,
|
||||
component: GiftViewSheetComponent(
|
||||
@ -2556,6 +2776,9 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
shareGift: {
|
||||
shareGiftImpl?()
|
||||
},
|
||||
resellGift: {
|
||||
resellGiftImpl?()
|
||||
},
|
||||
viewUpgraded: { messageId in
|
||||
viewUpgradedImpl?(messageId)
|
||||
},
|
||||
@ -2939,6 +3162,102 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
self.present(shareController, in: .window(.root))
|
||||
}
|
||||
|
||||
resellGiftImpl = { [weak self] in
|
||||
guard let self, let arguments = self.subject.arguments, case let .profileGift(peerId, currentSubject) = self.subject, case let .unique(gift) = arguments.gift else {
|
||||
return
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
if let resellStars = gift.resellStars, resellStars > 0 {
|
||||
let alertController = textAlertController(
|
||||
context: context,
|
||||
title: "Unlist This Item?",
|
||||
text: "It will no longer be for sale.",
|
||||
actions: [
|
||||
TextAlertAction(type: .defaultAction, title: "Unlist", action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(nil))))
|
||||
|
||||
let giftTitle = "\(gift.title) #\(gift.number)"
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let text = "\(giftTitle) is removed from sale."
|
||||
let tooltipController = UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .universalImage(
|
||||
image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Unlist"), color: .white)!,
|
||||
size: nil,
|
||||
title: nil,
|
||||
text: text,
|
||||
customUndoText: nil,
|
||||
timeout: 3.0
|
||||
),
|
||||
position: .bottom,
|
||||
animateInAsReplacement: false,
|
||||
appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0),
|
||||
action: { action in
|
||||
return false
|
||||
}
|
||||
)
|
||||
self.present(tooltipController, in: .window(.root))
|
||||
|
||||
if let updateResellStars {
|
||||
updateResellStars(nil)
|
||||
} else {
|
||||
let _ = (context.engine.payments.updateStarGiftResalePrice(slug: gift.slug, price: nil)
|
||||
|> deliverOnMainQueue).startStandalone()
|
||||
}
|
||||
}),
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
})
|
||||
],
|
||||
actionLayout: .vertical
|
||||
)
|
||||
self.present(alertController, in: .window(.root))
|
||||
} else {
|
||||
let resellController = context.sharedContext.makeStarGiftResellScreen(context: context, completion: { [weak self] price in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(price))))
|
||||
|
||||
let giftTitle = "\(gift.title) #\(gift.number)"
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let text = "\(giftTitle) is now for sale!"
|
||||
|
||||
let tooltipController = UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .universalImage(
|
||||
image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Sell"), color: .white)!,
|
||||
size: nil,
|
||||
title: nil,
|
||||
text: text,
|
||||
customUndoText: nil,
|
||||
timeout: 3.0
|
||||
),
|
||||
position: .bottom,
|
||||
animateInAsReplacement: false,
|
||||
appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0),
|
||||
action: { action in
|
||||
return false
|
||||
}
|
||||
)
|
||||
self.present(tooltipController, in: .window(.root))
|
||||
|
||||
if let updateResellStars {
|
||||
updateResellStars(price)
|
||||
} else {
|
||||
let _ = (context.engine.payments.updateStarGiftResalePrice(slug: gift.slug, price: price)
|
||||
|> deliverOnMainQueue).startStandalone()
|
||||
}
|
||||
})
|
||||
self.push(resellController)
|
||||
}
|
||||
}
|
||||
|
||||
viewUpgradedImpl = { [weak self] messageId in
|
||||
guard let self, let navigationController = self.navigationController as? NavigationController else {
|
||||
return
|
||||
@ -3066,6 +3385,13 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.view.disablesInteractiveModalDismiss = true
|
||||
|
||||
if let arguments = self.subject.arguments, let _ = self.subject.arguments?.resellStars {
|
||||
if case let .unique(uniqueGift) = arguments.gift, case .peerId(self.context.account.peerId) = uniqueGift.owner {
|
||||
} else {
|
||||
self.showBalance = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override func viewWillDisappear(_ animated: Bool) {
|
||||
|
@ -148,7 +148,7 @@ private final class GiftWithdrawAlertContentNode: AlertContentNode {
|
||||
theme: self.presentationTheme,
|
||||
strings: self.strings,
|
||||
peer: nil,
|
||||
subject: .uniqueGift(gift: self.gift),
|
||||
subject: .uniqueGift(gift: self.gift, price: nil),
|
||||
mode: .thumbnail
|
||||
)
|
||||
),
|
||||
|
19
submodules/TelegramUI/Components/MarqueeComponent/BUILD
Normal file
19
submodules/TelegramUI/Components/MarqueeComponent/BUILD
Normal file
@ -0,0 +1,19 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "MarqueeComponent",
|
||||
module_name = "MarqueeComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,180 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
|
||||
private let animationDuration: TimeInterval = 12.0
|
||||
private let animationDelay: TimeInterval = 2.0
|
||||
private let spacing: CGFloat = 20.0
|
||||
|
||||
public final class MarqueeComponent: Component {
|
||||
public static let innerPadding: CGFloat = 16.0
|
||||
|
||||
private final class MeasureState: Equatable {
|
||||
let attributedText: NSAttributedString
|
||||
let availableSize: CGSize
|
||||
let size: CGSize
|
||||
|
||||
init(attributedText: NSAttributedString, availableSize: CGSize, size: CGSize) {
|
||||
self.attributedText = attributedText
|
||||
self.availableSize = availableSize
|
||||
self.size = size
|
||||
}
|
||||
|
||||
static func ==(lhs: MeasureState, rhs: MeasureState) -> Bool {
|
||||
if !lhs.attributedText.isEqual(rhs.attributedText) {
|
||||
return false
|
||||
}
|
||||
if lhs.availableSize != rhs.availableSize {
|
||||
return false
|
||||
}
|
||||
if lhs.size != rhs.size {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var measureState: MeasureState?
|
||||
private let containerLayer = SimpleLayer()
|
||||
private let textLayer = SimpleLayer()
|
||||
private let duplicateTextLayer = SimpleLayer()
|
||||
private let gradientMaskLayer = SimpleGradientLayer()
|
||||
private var isAnimating = false
|
||||
private var isOverflowing = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.containerLayer.masksToBounds = true
|
||||
self.layer.addSublayer(self.containerLayer)
|
||||
|
||||
self.containerLayer.addSublayer(self.textLayer)
|
||||
self.containerLayer.addSublayer(self.duplicateTextLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func update(component: MarqueeComponent, availableSize: CGSize) -> CGSize {
|
||||
let attributedText = component.attributedText
|
||||
if let measureState = self.measureState {
|
||||
if measureState.attributedText.isEqual(to: attributedText) && measureState.availableSize == availableSize {
|
||||
return measureState.size
|
||||
}
|
||||
}
|
||||
|
||||
var boundingRect = attributedText.boundingRect(with: CGSize(width: 10000, height: availableSize.height), options: .usesLineFragmentOrigin, context: nil)
|
||||
boundingRect.size.width = ceil(boundingRect.size.width)
|
||||
boundingRect.size.height = ceil(boundingRect.size.height)
|
||||
|
||||
let measureState = MeasureState(attributedText: attributedText, availableSize: availableSize, size: boundingRect.size)
|
||||
self.measureState = measureState
|
||||
|
||||
self.containerLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: measureState.size.width + innerPadding * 2.0, height: measureState.size.height))
|
||||
|
||||
let isOverflowing = boundingRect.width > availableSize.width
|
||||
|
||||
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: measureState.size))
|
||||
let image = renderer.image { context in
|
||||
UIGraphicsPushContext(context.cgContext)
|
||||
measureState.attributedText.draw(at: CGPoint())
|
||||
UIGraphicsPopContext()
|
||||
}
|
||||
|
||||
if isOverflowing {
|
||||
self.setupMarqueeTextLayers(textImage: image.cgImage!, textWidth: boundingRect.width, containerWidth: availableSize.width)
|
||||
self.setupGradientMask(size: CGSize(width: availableSize.width, height: boundingRect.height))
|
||||
self.startAnimation()
|
||||
} else {
|
||||
self.stopAnimation()
|
||||
self.textLayer.frame = CGRect(origin: CGPoint(x: innerPadding, y: 0.0), size: boundingRect.size)
|
||||
self.textLayer.contents = image.cgImage
|
||||
self.duplicateTextLayer.frame = .zero
|
||||
self.duplicateTextLayer.contents = nil
|
||||
self.layer.mask = nil
|
||||
}
|
||||
|
||||
return CGSize(width: min(measureState.size.width + innerPadding * 2.0, availableSize.width), height: measureState.size.height)
|
||||
}
|
||||
|
||||
private func setupMarqueeTextLayers(textImage: CGImage, textWidth: CGFloat, containerWidth: CGFloat) {
|
||||
self.textLayer.frame = CGRect(x: innerPadding, y: 0, width: textWidth, height: self.containerLayer.bounds.height)
|
||||
self.textLayer.contents = textImage
|
||||
|
||||
self.duplicateTextLayer.frame = CGRect(x: innerPadding + textWidth + spacing, y: 0, width: textWidth, height: self.containerLayer.bounds.height)
|
||||
self.duplicateTextLayer.contents = textImage
|
||||
|
||||
self.containerLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: textWidth * 2.0 + spacing, height: self.containerLayer.bounds.height))
|
||||
}
|
||||
|
||||
private func setupGradientMask(size: CGSize) {
|
||||
self.gradientMaskLayer.frame = CGRect(origin: .zero, size: size)
|
||||
self.gradientMaskLayer.colors = [
|
||||
UIColor.clear.cgColor,
|
||||
UIColor.clear.cgColor,
|
||||
UIColor.black.cgColor,
|
||||
UIColor.black.cgColor,
|
||||
UIColor.clear.cgColor,
|
||||
UIColor.clear.cgColor
|
||||
]
|
||||
self.gradientMaskLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
|
||||
self.gradientMaskLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
|
||||
|
||||
let edgePercentage = innerPadding / size.width
|
||||
self.gradientMaskLayer.locations = [
|
||||
0.0,
|
||||
NSNumber(value: edgePercentage * 0.4),
|
||||
NSNumber(value: edgePercentage),
|
||||
NSNumber(value: 1.0 - edgePercentage),
|
||||
NSNumber(value: 1.0 - edgePercentage * 0.4),
|
||||
1.0
|
||||
]
|
||||
|
||||
self.layer.mask = self.gradientMaskLayer
|
||||
}
|
||||
|
||||
private func startAnimation() {
|
||||
guard !self.isAnimating else {
|
||||
return
|
||||
}
|
||||
self.isAnimating = true
|
||||
|
||||
self.containerLayer.removeAllAnimations()
|
||||
|
||||
self.containerLayer.animateBoundsOriginXAdditive(from: 0.0, to: self.textLayer.frame.width + spacing, duration: animationDuration, delay: animationDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, completion: { _ in
|
||||
self.isAnimating = false
|
||||
self.startAnimation()
|
||||
})
|
||||
}
|
||||
|
||||
private func stopAnimation() {
|
||||
self.containerLayer.removeAllAnimations()
|
||||
self.isAnimating = false
|
||||
}
|
||||
}
|
||||
|
||||
public let attributedText: NSAttributedString
|
||||
|
||||
public init(attributedText: NSAttributedString) {
|
||||
self.attributedText = attributedText
|
||||
}
|
||||
|
||||
public static func ==(lhs: MarqueeComponent, rhs: MarqueeComponent) -> Bool {
|
||||
if lhs.attributedText != rhs.attributedText {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize)
|
||||
}
|
||||
}
|
@ -326,6 +326,8 @@ final class MediaEditorScreenComponent: Component {
|
||||
|
||||
private let switchCameraButton = ComponentView<Empty>()
|
||||
|
||||
private let selectionButton = ComponentView<Empty>()
|
||||
|
||||
private let textCancelButton = ComponentView<Empty>()
|
||||
private let textDoneButton = ComponentView<Empty>()
|
||||
private let textSize = ComponentView<Empty>()
|
||||
@ -335,6 +337,8 @@ final class MediaEditorScreenComponent: Component {
|
||||
private var isEditingCaption = false
|
||||
private var currentInputMode: MessageInputPanelComponent.InputMode = .text
|
||||
|
||||
private var isSelectionPanelOpen = false
|
||||
|
||||
private var didInitializeInputMediaNodeDataPromise = false
|
||||
private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData?
|
||||
private var inputMediaNodeDataPromise = Promise<ChatEntityKeyboardInputNode.InputData>()
|
||||
@ -1988,6 +1992,40 @@ final class MediaEditorScreenComponent: Component {
|
||||
transition.setScale(view: switchCameraButtonView, scale: isRecordingAdditionalVideo ? 1.0 : 0.01)
|
||||
transition.setAlpha(view: switchCameraButtonView, alpha: isRecordingAdditionalVideo ? 1.0 : 0.0)
|
||||
}
|
||||
|
||||
|
||||
let selectionButtonSize = self.selectionButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
SelectionPanelButtonContentComponent(
|
||||
count: 1,
|
||||
isSelected: self.isSelectionPanelOpen,
|
||||
tag: nil
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
self.isSelectionPanelOpen = !self.isSelectionPanelOpen
|
||||
self.state?.updated()
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 33.0, height: 33.0)
|
||||
)
|
||||
let selectionButtonFrame = CGRect(
|
||||
origin: CGPoint(x: availableSize.width - selectionButtonSize.width - 12.0, y: max(environment.statusBarHeight + 10.0, inputPanelFrame.minY - selectionButtonSize.height - 3.0)),
|
||||
size: selectionButtonSize
|
||||
)
|
||||
if let selectionButtonView = self.selectionButton.view {
|
||||
if selectionButtonView.superview == nil {
|
||||
self.addSubview(selectionButtonView)
|
||||
}
|
||||
transition.setPosition(view: selectionButtonView, position: selectionButtonFrame.center)
|
||||
transition.setBounds(view: selectionButtonView, bounds: CGRect(origin: .zero, size: selectionButtonFrame.size))
|
||||
}
|
||||
} else {
|
||||
inputPanelSize = CGSize(width: 0.0, height: 12.0)
|
||||
}
|
||||
@ -3407,7 +3445,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
}
|
||||
} else if case let .gift(gift) = effectiveSubject {
|
||||
isGift = true
|
||||
let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, peerId: nil, senderId: nil, savedId: nil))]
|
||||
let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, peerId: nil, senderId: nil, savedId: nil, resaleStars: nil))]
|
||||
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: Namespaces.Message.Cloud, id: -1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
|
||||
messages = .single([message])
|
||||
} else {
|
||||
|
@ -0,0 +1,140 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
|
||||
final class SelectionPanelButtonContentComponent: Component {
|
||||
let count: Int32
|
||||
let isSelected: Bool
|
||||
let tag: AnyObject?
|
||||
|
||||
init(
|
||||
count: Int32,
|
||||
isSelected: Bool,
|
||||
tag: AnyObject?
|
||||
) {
|
||||
self.count = count
|
||||
self.isSelected = isSelected
|
||||
self.tag = tag
|
||||
}
|
||||
|
||||
static func ==(lhs: SelectionPanelButtonContentComponent, rhs: SelectionPanelButtonContentComponent) -> Bool {
|
||||
return lhs.count == rhs.count && lhs.isSelected == rhs.isSelected
|
||||
}
|
||||
|
||||
final class View: UIView, ComponentTaggedView {
|
||||
private var component: SelectionPanelButtonContentComponent?
|
||||
public func matches(tag: Any) -> Bool {
|
||||
if let component = self.component, let componentTag = component.tag {
|
||||
let tag = tag as AnyObject
|
||||
if componentTag === tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
private let outline = SimpleLayer()
|
||||
private let icon = SimpleLayer()
|
||||
private let label = ComponentView<Empty>()
|
||||
|
||||
init() {
|
||||
self.backgroundView = BlurredBackgroundView(color: UIColor(white: 0.2, alpha: 0.45), enableBlur: true)
|
||||
self.icon.opacity = 0.0
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
self.layer.addSublayer(self.icon)
|
||||
self.layer.addSublayer(self.outline)
|
||||
|
||||
self.outline.contents = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
context.clear(bounds)
|
||||
let lineWidth: CGFloat = 2.0 - UIScreenPixel
|
||||
context.setLineWidth(lineWidth)
|
||||
context.setStrokeColor(UIColor.white.cgColor)
|
||||
context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0))
|
||||
})?.cgImage
|
||||
|
||||
self.icon.contents = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
context.clear(bounds)
|
||||
let lineWidth: CGFloat = 2.0 - UIScreenPixel
|
||||
context.setLineWidth(lineWidth)
|
||||
context.setStrokeColor(UIColor.white.cgColor)
|
||||
|
||||
context.move(to: CGPoint(x: 11.0, y: 11.0))
|
||||
context.addLine(to: CGPoint(x: size.width - 11.0, y: size.height - 11.0))
|
||||
context.strokePath()
|
||||
|
||||
context.move(to: CGPoint(x: size.width - 11.0, y: 11.0))
|
||||
context.addLine(to: CGPoint(x: 11.0, y: size.height - 11.0))
|
||||
context.strokePath()
|
||||
})?.cgImage
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func update(component: SelectionPanelButtonContentComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
|
||||
let size = CGSize(width: 33.0, height: 33.0)
|
||||
let backgroundFrame = CGRect(origin: .zero, size: size)
|
||||
|
||||
self.backgroundView.frame = backgroundFrame
|
||||
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.width / 2.0, transition: .immediate)
|
||||
|
||||
self.icon.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||
self.icon.bounds = CGRect(origin: .zero, size: size)
|
||||
|
||||
self.outline.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||
self.outline.bounds = CGRect(origin: .zero, size: size)
|
||||
|
||||
let labelSize = self.label.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
Text(
|
||||
text: "\(component.count)",
|
||||
font: Font.with(size: 18.0, design: .round, weight: .semibold),
|
||||
color: .white
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: size
|
||||
)
|
||||
let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize)
|
||||
if let labelView = self.label.view {
|
||||
if labelView.superview == nil {
|
||||
self.addSubview(labelView)
|
||||
}
|
||||
labelView.center = labelFrame.center
|
||||
labelView.bounds = CGRect(origin: .zero, size: labelFrame.size)
|
||||
}
|
||||
|
||||
if (previousComponent?.isSelected ?? false) != component.isSelected {
|
||||
let changeTransition: ComponentTransition = .easeInOut(duration: 0.2)
|
||||
changeTransition.setAlpha(layer: self.icon, alpha: component.isSelected ? 1.0 : 0.0)
|
||||
changeTransition.setTransform(layer: self.icon, transform: !component.isSelected ? CATransform3DMakeRotation(.pi / 4.0, 0.0, 0.0, 1.0) : CATransform3DIdentity)
|
||||
if let labelView = self.label.view {
|
||||
changeTransition.setAlpha(view: labelView, alpha: component.isSelected ? 0.0 : 1.0)
|
||||
changeTransition.setTransform(view: labelView, transform: component.isSelected ? CATransform3DMakeRotation(-.pi / 4.0, 0.0, 0.0, 1.0) : CATransform3DIdentity)
|
||||
}
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
|
||||
|
@ -497,7 +497,7 @@ private class GiftIconLayer: SimpleLayer {
|
||||
for attribute in gift.attributes {
|
||||
if case let .model(_, fileValue, _) = attribute {
|
||||
file = fileValue
|
||||
} else if case let .backdrop(_, innerColor, _, _, _, _) = attribute {
|
||||
} else if case let .backdrop(_, _, innerColor, _, _, _, _) = attribute {
|
||||
color = UIColor(rgb: UInt32(bitPattern: innerColor))
|
||||
}
|
||||
}
|
||||
@ -563,7 +563,7 @@ private class GiftIconLayer: SimpleLayer {
|
||||
for attribute in gift.attributes {
|
||||
if case let .model(_, fileValue, _) = attribute {
|
||||
file = fileValue
|
||||
} else if case let .backdrop(_, innerColor, _, _, _, _) = attribute {
|
||||
} else if case let .backdrop(_, _, innerColor, _, _, _, _) = attribute {
|
||||
color = UIColor(rgb: UInt32(bitPattern: innerColor))
|
||||
}
|
||||
}
|
||||
|
@ -477,42 +477,52 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
itemTransition = .immediate
|
||||
}
|
||||
|
||||
let ribbonText: String?
|
||||
var ribbonText: String?
|
||||
var ribbonColor: GiftItemComponent.Ribbon.Color = .blue
|
||||
var ribbonFont: GiftItemComponent.Ribbon.Font = .generic
|
||||
var ribbonOutline: UIColor?
|
||||
|
||||
let peer: GiftItemComponent.Peer?
|
||||
let subject: GiftItemComponent.Subject
|
||||
var resellPrice: Int64?
|
||||
|
||||
switch product.gift {
|
||||
case let .generic(gift):
|
||||
subject = .starGift(gift: gift, price: "⭐️ \(gift.price)")
|
||||
peer = product.fromPeer.flatMap { .peer($0) } ?? .anonymous
|
||||
|
||||
if let availability = gift.availability {
|
||||
ribbonText = params.presentationData.strings.PeerInfo_Gifts_OneOf(compactNumericCountString(Int(availability.total), decimalSeparator: params.presentationData.dateTimeFormat.decimalSeparator)).string
|
||||
} else {
|
||||
ribbonText = nil
|
||||
}
|
||||
case let .unique(gift):
|
||||
if product.pinnedToTop {
|
||||
ribbonFont = .monospaced
|
||||
ribbonText = "#\(gift.number)"
|
||||
subject = .uniqueGift(gift: gift, price: nil)
|
||||
peer = nil
|
||||
resellPrice = gift.resellStars
|
||||
|
||||
if let _ = resellPrice {
|
||||
//TODO:localize
|
||||
ribbonText = "sale"
|
||||
ribbonFont = .larger
|
||||
ribbonColor = .green
|
||||
ribbonOutline = params.presentationData.theme.list.blocksBackgroundColor
|
||||
} else {
|
||||
ribbonText = params.presentationData.strings.PeerInfo_Gifts_OneOf(compactNumericCountString(Int(gift.availability.issued), decimalSeparator: params.presentationData.dateTimeFormat.decimalSeparator)).string
|
||||
}
|
||||
for attribute in gift.attributes {
|
||||
if case let .backdrop(_, innerColor, outerColor, _, _, _) = attribute {
|
||||
ribbonColor = .custom(outerColor, innerColor)
|
||||
break
|
||||
if product.pinnedToTop {
|
||||
ribbonFont = .monospaced
|
||||
ribbonText = "#\(gift.number)"
|
||||
} else {
|
||||
ribbonText = params.presentationData.strings.PeerInfo_Gifts_OneOf(compactNumericCountString(Int(gift.availability.issued), decimalSeparator: params.presentationData.dateTimeFormat.decimalSeparator)).string
|
||||
}
|
||||
for attribute in gift.attributes {
|
||||
if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute {
|
||||
ribbonColor = .custom(outerColor, innerColor)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let peer: GiftItemComponent.Peer?
|
||||
let subject: GiftItemComponent.Subject
|
||||
switch product.gift {
|
||||
case let .generic(gift):
|
||||
subject = .starGift(gift: gift, price: "⭐️ \(gift.price)")
|
||||
peer = product.fromPeer.flatMap { .peer($0) } ?? .anonymous
|
||||
case let .unique(gift):
|
||||
subject = .uniqueGift(gift: gift)
|
||||
peer = nil
|
||||
}
|
||||
|
||||
|
||||
let _ = visibleItem.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(
|
||||
@ -522,7 +532,8 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
strings: params.presentationData.strings,
|
||||
peer: peer,
|
||||
subject: subject,
|
||||
ribbon: ribbonText.flatMap { GiftItemComponent.Ribbon(text: $0, font: ribbonFont, color: ribbonColor) },
|
||||
ribbon: ribbonText.flatMap { GiftItemComponent.Ribbon(text: $0, font: ribbonFont, color: ribbonColor, outline: ribbonOutline) },
|
||||
resellPrice: resellPrice,
|
||||
isHidden: !product.savedToProfile,
|
||||
isPinned: product.pinnedToTop,
|
||||
isEditing: self.isReordering,
|
||||
@ -589,6 +600,12 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
}
|
||||
return self.profileGifts.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo)
|
||||
},
|
||||
updateResellStars: { [weak self] price in
|
||||
guard let self, case let .unique(uniqueGift) = product.gift else {
|
||||
return
|
||||
}
|
||||
self.profileGifts.updateStarGiftResellPrice(slug: uniqueGift.slug, price: price)
|
||||
},
|
||||
togglePinnedToTop: { [weak self] pinnedToTop in
|
||||
guard let self else {
|
||||
return false
|
||||
|
@ -125,7 +125,7 @@ final class GiftListItemComponent: Component {
|
||||
theme: component.theme,
|
||||
strings: component.context.sharedContext.currentPresentationData.with { $0 }.strings,
|
||||
peer: nil,
|
||||
subject: .uniqueGift(gift: gift),
|
||||
subject: .uniqueGift(gift: gift, price: nil),
|
||||
ribbon: nil,
|
||||
isHidden: false,
|
||||
isSelected: gift.id == component.selectedId,
|
||||
|
@ -456,10 +456,11 @@ final class UserAppearanceScreenComponent: Component {
|
||||
attributes: [
|
||||
.model(name: "", file: file, rarity: 0),
|
||||
.pattern(name: "", file: patternFile, rarity: 0),
|
||||
.backdrop(name: "", innerColor: innerColor, outerColor: outerColor, patternColor: patternColor, textColor: textColor, rarity: 0)
|
||||
.backdrop(name: "", id: 0, innerColor: innerColor, outerColor: outerColor, patternColor: patternColor, textColor: textColor, rarity: 0)
|
||||
],
|
||||
availability: StarGift.UniqueGift.Availability(issued: 0, total: 0),
|
||||
giftAddress: nil
|
||||
giftAddress: nil,
|
||||
resellStars: nil
|
||||
)
|
||||
signal = component.context.engine.accountData.setStarGiftStatus(starGift: gift, expirationDate: emojiStatus.expirationDate)
|
||||
} else {
|
||||
@ -1090,7 +1091,7 @@ final class UserAppearanceScreenComponent: Component {
|
||||
case let .pattern(_, file, _):
|
||||
patternFileId = file.fileId.id
|
||||
self.cachedIconFiles[file.fileId.id] = file
|
||||
case let .backdrop(_, innerColorValue, outerColorValue, patternColorValue, textColorValue, _):
|
||||
case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, textColorValue, _):
|
||||
innerColor = innerColorValue
|
||||
outerColor = outerColorValue
|
||||
patternColor = patternColorValue
|
||||
|
@ -126,7 +126,7 @@ public final class StarsAvatarComponent: Component {
|
||||
theme: component.theme,
|
||||
strings: component.context.sharedContext.currentPresentationData.with { $0 }.strings,
|
||||
peer: nil,
|
||||
subject: .uniqueGift(gift: gift),
|
||||
subject: .uniqueGift(gift: gift, price: nil),
|
||||
mode: .thumbnail
|
||||
)
|
||||
),
|
||||
|
@ -105,7 +105,8 @@ private final class SheetContent: CombinedComponent {
|
||||
let minAmount: StarsAmount?
|
||||
let maxAmount: StarsAmount?
|
||||
|
||||
let configuration = StarsWithdrawConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
|
||||
let withdrawConfiguration = StarsWithdrawConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
|
||||
let resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
|
||||
|
||||
switch component.mode {
|
||||
case let .withdraw(status):
|
||||
@ -113,7 +114,7 @@ private final class SheetContent: CombinedComponent {
|
||||
amountTitle = environment.strings.Stars_Withdraw_AmountTitle
|
||||
amountPlaceholder = environment.strings.Stars_Withdraw_AmountPlaceholder
|
||||
|
||||
minAmount = configuration.minWithdrawAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
|
||||
minAmount = withdrawConfiguration.minWithdrawAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
|
||||
maxAmount = status.balances.availableBalance
|
||||
amountLabel = nil
|
||||
case .accountWithdraw:
|
||||
@ -121,7 +122,7 @@ private final class SheetContent: CombinedComponent {
|
||||
amountTitle = environment.strings.Stars_Withdraw_AmountTitle
|
||||
amountPlaceholder = environment.strings.Stars_Withdraw_AmountPlaceholder
|
||||
|
||||
minAmount = configuration.minWithdrawAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
|
||||
minAmount = withdrawConfiguration.minWithdrawAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
|
||||
maxAmount = state.balance
|
||||
amountLabel = nil
|
||||
case .paidMedia:
|
||||
@ -130,9 +131,9 @@ private final class SheetContent: CombinedComponent {
|
||||
amountPlaceholder = environment.strings.Stars_PaidContent_AmountPlaceholder
|
||||
|
||||
minAmount = StarsAmount(value: 1, nanos: 0)
|
||||
maxAmount = configuration.maxPaidMediaAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
|
||||
maxAmount = withdrawConfiguration.maxPaidMediaAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
|
||||
|
||||
if let usdWithdrawRate = configuration.usdWithdrawRate, let amount = state.amount, amount > StarsAmount.zero {
|
||||
if let usdWithdrawRate = withdrawConfiguration.usdWithdrawRate, let amount = state.amount, amount > StarsAmount.zero {
|
||||
let usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0
|
||||
amountLabel = "≈\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))"
|
||||
} else {
|
||||
@ -144,7 +145,16 @@ private final class SheetContent: CombinedComponent {
|
||||
amountPlaceholder = environment.strings.Stars_SendStars_AmountPlaceholder
|
||||
|
||||
minAmount = StarsAmount(value: 1, nanos: 0)
|
||||
maxAmount = configuration.maxPaidMediaAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
|
||||
maxAmount = withdrawConfiguration.maxPaidMediaAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
|
||||
amountLabel = nil
|
||||
case .starGiftResell:
|
||||
//TODO:localize
|
||||
titleString = "Sell Gift"
|
||||
amountTitle = "PRICE IN STARS"
|
||||
amountPlaceholder = "Enter Price"
|
||||
|
||||
minAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMinAmount, nanos: 0)
|
||||
maxAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMaxAmount, nanos: 0)
|
||||
amountLabel = nil
|
||||
}
|
||||
|
||||
@ -214,10 +224,16 @@ private final class SheetContent: CombinedComponent {
|
||||
}
|
||||
|
||||
let amountFont = Font.regular(13.0)
|
||||
let boldAmountFont = Font.semibold(13.0)
|
||||
let amountTextColor = theme.list.freeTextColor
|
||||
let amountMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor), bold: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor), link: MarkdownAttributeSet(font: amountFont, textColor: theme.list.itemAccentColor), linkAttribute: { contents in
|
||||
let amountMarkdownAttributes = MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor),
|
||||
bold: MarkdownAttributeSet(font: boldAmountFont, textColor: amountTextColor),
|
||||
link: MarkdownAttributeSet(font: amountFont, textColor: theme.list.itemAccentColor),
|
||||
linkAttribute: { contents in
|
||||
return (TelegramTextAttributes.URL, contents)
|
||||
})
|
||||
}
|
||||
)
|
||||
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme {
|
||||
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme)
|
||||
}
|
||||
@ -252,6 +268,18 @@ private final class SheetContent: CombinedComponent {
|
||||
text: .plain(amountInfoString),
|
||||
maximumNumberOfLines: 0
|
||||
))
|
||||
case .starGiftResell:
|
||||
//TODO:localize
|
||||
let amountInfoString: NSAttributedString
|
||||
if let value = state.amount?.value, value > 0 {
|
||||
amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString("You will receive **\(Int32(floor(Float(value) * 0.8))) Stars**.", attributes: amountMarkdownAttributes, textAlignment: .natural))
|
||||
} else {
|
||||
amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString("You will receive **80%**.", attributes: amountMarkdownAttributes, textAlignment: .natural))
|
||||
}
|
||||
amountFooter = AnyComponent(MultilineTextComponent(
|
||||
text: .plain(amountInfoString),
|
||||
maximumNumberOfLines: 0
|
||||
))
|
||||
default:
|
||||
amountFooter = nil
|
||||
}
|
||||
@ -305,8 +333,15 @@ private final class SheetContent: CombinedComponent {
|
||||
let buttonString: String
|
||||
if case .paidMedia = component.mode {
|
||||
buttonString = environment.strings.Stars_PaidContent_Create
|
||||
} else if case .starGiftResell = component.mode {
|
||||
//TODO:localize
|
||||
if let amount = state.amount, amount.value > 0 {
|
||||
buttonString = "Sell for # \(presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator))"
|
||||
} else {
|
||||
buttonString = "Sell"
|
||||
}
|
||||
} else if let amount = state.amount {
|
||||
buttonString = "\(environment.strings.Stars_Withdraw_Withdraw) # \(presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator))"
|
||||
buttonString = "\(environment.strings.Stars_Withdraw_Withdraw) # \(presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator))"
|
||||
} else {
|
||||
buttonString = environment.strings.Stars_Withdraw_Withdraw
|
||||
}
|
||||
@ -318,10 +353,17 @@ private final class SheetContent: CombinedComponent {
|
||||
let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
|
||||
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))
|
||||
buttonAttributedString.addAttribute(.foregroundColor, value: theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string))
|
||||
buttonAttributedString.addAttribute(.baselineOffset, value: 1.5, 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(
|
||||
component: ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
@ -394,6 +436,8 @@ private final class SheetContent: CombinedComponent {
|
||||
amount = initialValue.flatMap { StarsAmount(value: $0, nanos: 0) }
|
||||
case .reaction:
|
||||
amount = nil
|
||||
case .starGiftResell:
|
||||
amount = nil
|
||||
}
|
||||
|
||||
self.amount = amount
|
||||
@ -514,6 +558,7 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
|
||||
case accountWithdraw
|
||||
case paidMedia(Int64?)
|
||||
case reaction(Int64?)
|
||||
case starGiftResell
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Peer Info/SortNumber.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Peer Info/SortNumber.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "hash.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/SortNumber.imageset/hash.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/SortNumber.imageset/hash.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Premium/Collectible/Sell.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Premium/Collectible/Sell.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "sale.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Premium/Collectible/Sell.imageset/sale.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Premium/Collectible/Sell.imageset/sale.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Premium/Collectible/Unlist.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Premium/Collectible/Unlist.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "unsale.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Premium/Collectible/Unlist.imageset/unsale.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Premium/Collectible/Unlist.imageset/unsale.pdf
vendored
Normal file
Binary file not shown.
@ -1124,7 +1124,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
let sendGiftTitle: String
|
||||
var isIncoming = message.effectivelyIncoming(context.account.peerId)
|
||||
for media in message.media {
|
||||
if let action = media as? TelegramMediaAction, case let .starGiftUnique(_, isUpgrade, _, _, _, _, _, _, _, _) = action.action {
|
||||
if let action = media as? TelegramMediaAction, case let .starGiftUnique(_, isUpgrade, _, _, _, _, _, _, _, _, _) = action.action {
|
||||
if isUpgrade && message.author?.id == context.account.peerId {
|
||||
isIncoming = true
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
@ -17,6 +18,7 @@ import TelegramBaseController
|
||||
import ContextUI
|
||||
import SliderContextItem
|
||||
import UndoUI
|
||||
import MarqueeComponent
|
||||
|
||||
private func normalizeValue(_ value: CGFloat) -> CGFloat {
|
||||
return round(value * 10.0) / 10.0
|
||||
@ -147,6 +149,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
private let albumArtNode: TransformImageNode
|
||||
private var largeAlbumArtNode: TransformImageNode?
|
||||
private let titleNode: TextNode
|
||||
private let title: ComponentView<Empty>
|
||||
private let descriptionNode: TextNode
|
||||
private let shareNode: HighlightableButtonNode
|
||||
private let artistButton: HighlightTrackingButtonNode
|
||||
@ -236,6 +239,8 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
|
||||
self.title = ComponentView<Empty>()
|
||||
|
||||
self.descriptionNode = TextNode()
|
||||
self.descriptionNode.isUserInteractionEnabled = false
|
||||
self.descriptionNode.displaysAsynchronously = false
|
||||
@ -295,7 +300,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
self.addSubnode(self.collapseNode)
|
||||
|
||||
self.addSubnode(self.albumArtNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
//self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.descriptionNode)
|
||||
self.addSubnode(self.artistButton)
|
||||
self.addSubnode(self.shareNode)
|
||||
@ -725,8 +730,25 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
self.artistButton.isUserInteractionEnabled = hasArtist
|
||||
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - sideInset * 2.0 - leftInset - rightInset - infoLabelsLeftInset - infoLabelsRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MarqueeComponent(attributedText: titleString ?? NSAttributedString())
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: width - sideInset * 2.0 - leftInset - rightInset - infoLabelsLeftInset - infoLabelsRightInset + MarqueeComponent.innerPadding, height: CGFloat.greatestFiniteMagnitude)
|
||||
)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.view.addSubview(titleView)
|
||||
}
|
||||
transition.updateFrame(view: titleView, frame: CGRect(origin: CGPoint(x: self.isExpanded ? floor((width - titleSize.width) / 2.0) : (leftInset + sideInset + infoLabelsLeftInset) - MarqueeComponent.innerPadding, y: infoVerticalOrigin + 1.0), size: titleSize))
|
||||
}
|
||||
|
||||
let makeDescriptionLayout = TextNode.asyncLayout(self.descriptionNode)
|
||||
let (descriptionLayout, descriptionApply) = makeDescriptionLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - sideInset * 2.0 - leftInset - rightInset - infoLabelsLeftInset - infoLabelsRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
|
@ -81,6 +81,7 @@ import AccountFreezeInfoScreen
|
||||
import JoinSubjectScreen
|
||||
import OldChannelsController
|
||||
import InviteLinksUI
|
||||
import GiftStoreScreen
|
||||
|
||||
private final class AccountUserInterfaceInUseContext {
|
||||
let subscribers = Bag<(Bool) -> Void>()
|
||||
@ -3267,6 +3268,15 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
return controller
|
||||
}
|
||||
|
||||
public func makeGiftStoreController(context: AccountContext, peerId: EnginePeer.Id, gift: StarGift.Gift) -> ViewController {
|
||||
guard let starsContext = context.starsContext else {
|
||||
fatalError()
|
||||
}
|
||||
let controller = GiftStoreScreen(context: context, starsContext: starsContext, peerId: peerId, gift: gift)
|
||||
controller.navigationPresentation = .modal
|
||||
return controller
|
||||
}
|
||||
|
||||
public func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController {
|
||||
let mappedSubject: PremiumPrivacyScreen.Subject
|
||||
let introSource: PremiumIntroSource
|
||||
@ -3656,6 +3666,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
return StarsWithdrawScreen(context: context, mode: .accountWithdraw, completion: completion)
|
||||
}
|
||||
|
||||
public func makeStarGiftResellScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController {
|
||||
return StarsWithdrawScreen(context: context, mode: .starGiftResell, completion: completion)
|
||||
}
|
||||
|
||||
public func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController {
|
||||
return StarsTransactionScreen(context: context, subject: .gift(message))
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user