[WIP] Gift resale

This commit is contained in:
Ilya Laktyushin 2025-04-12 02:19:32 +04:00
parent da86483d66
commit b130511450
46 changed files with 3554 additions and 196 deletions

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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(

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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(

View File

@ -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) {

View File

@ -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
}
}

View File

@ -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)

View File

@ -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,

View File

@ -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))

View File

@ -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
)
),

View File

@ -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
}

View File

@ -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))

View File

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

View File

@ -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,12 +450,26 @@ 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)
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) {
@ -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()
@ -756,33 +863,33 @@ 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))
}

View 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",
],
)

View File

@ -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
}
}
}

View File

@ -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))
}
}

View File

@ -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
)
),

View File

@ -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
)

View File

@ -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,6 +2733,7 @@ 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)?
@ -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) {

View File

@ -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
)
),

View 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",
],
)

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -0,0 +1,7 @@
import Foundation
import UIKit
import Display
import ComponentFlow

View File

@ -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))
}
}

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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
)
),

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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
}

View File

@ -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()))

View File

@ -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))
}