mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Paid media improvements
This commit is contained in:
parent
a545eac99e
commit
b5b052ff7d
63 changed files with 3460 additions and 847 deletions
|
|
@ -12290,12 +12290,14 @@ Sorry for the inconvenience.";
|
|||
"Stars.Transaction.Via" = "Via";
|
||||
"Stars.Transaction.To" = "To";
|
||||
"Stars.Transaction.From" = "From";
|
||||
"Stars.Transaction.Media" = "Media";
|
||||
"Stars.Transaction.Id" = "Transaction ID";
|
||||
"Stars.Transaction.Date" = "Date";
|
||||
"Stars.Transaction.Terms" = "Review the [Terms of Service]() for Stars.";
|
||||
"Stars.Transaction.Terms_URL" = "https://telegram.org/tos";
|
||||
"Stars.Transaction.CopiedId" = "Transaction ID copied to clipboard.";
|
||||
|
||||
"Stars.Transaction.MediaPurchase" = "Media Purchase";
|
||||
"Stars.Transaction.AppleTopUp.Title" = "Stars Top-Up";
|
||||
"Stars.Transaction.AppleTopUp.Subtitle" = "App Store";
|
||||
"Stars.Transaction.GoogleTopUp.Title" = "Stars Top-Up";
|
||||
|
|
@ -12319,7 +12321,7 @@ Sorry for the inconvenience.";
|
|||
"Stars.Transfer.Purchased.Stars_1" = "%@ Star";
|
||||
"Stars.Transfer.Purchased.Stars_any" = "%@ Stars";
|
||||
"Stars.Transfer.UnlockedText" = "You unlocked media for **%1$@**.";
|
||||
"Stars.Transfer.UnlockInfo" = "Do you want to unlock media for **%1$@**?";
|
||||
"Stars.Transfer.UnlockInfo" = "Do you want to unlock %1$@ in **%2$@** for **%3$@**?";
|
||||
|
||||
"Stars.Transfer.Balance" = "Balance";
|
||||
|
||||
|
|
@ -12349,16 +12351,16 @@ Sorry for the inconvenience.";
|
|||
"HashtagSearch.StoriesFoundInfo" = "View stories with %@";
|
||||
|
||||
"Stars.BotRevenue.Title" = "Stars Balance";
|
||||
"Stars.BotRevenue.Revenue.Title" = "Revenue";
|
||||
"Stars.BotRevenue.Proceeds.Title" = "Proceeds Overview";
|
||||
"Stars.BotRevenue.Revenue.Title" = "Stars Received";
|
||||
"Stars.BotRevenue.Proceeds.Title" = "Rewards Overview";
|
||||
"Stars.BotRevenue.Proceeds.Available" = "Available Balance";
|
||||
"Stars.BotRevenue.Proceeds.Current" = "Total Balance";
|
||||
"Stars.BotRevenue.Proceeds.Total" = "Total Lifetime Proceeds";
|
||||
"Stars.BotRevenue.Proceeds.Info" = "Stars from your total balance become available for spending on ads and rewards 21 days after they are earned.";
|
||||
"Stars.BotRevenue.Proceeds.Total" = "Lifetime Proceeds";
|
||||
"Stars.BotRevenue.Proceeds.Info" = "Stars from your total balance can be used for ads or withdrawn as rewards 21 days after they are earned.";
|
||||
|
||||
"Stars.BotRevenue.Withdraw.Balance" = "Available Balance";
|
||||
"Stars.BotRevenue.Withdraw.Withdraw" = "Withdraw via Fragment";
|
||||
"Stars.BotRevenue.Withdraw.Info" = "You can withdraw Stars using Fragment, or use Stars to advertise your bot. [Learn More >]()";
|
||||
"Stars.BotRevenue.Withdraw.Info" = "You can collect rewards for Stars using Fragment, or use Stars to advertise your bot. [Learn More >]()";
|
||||
"Stars.BotRevenue.Withdraw.Info_URL" = "https://telegram.org/tos";
|
||||
|
||||
"Stars.BotRevenue.Transactions.Title" = "Transaction History";
|
||||
|
|
@ -12403,3 +12405,18 @@ Sorry for the inconvenience.";
|
|||
|
||||
"Chat.MessagesDeletedToast.Text_1" = "Message Deleted";
|
||||
"Chat.MessagesDeletedToast.Text_any" = "%d Messages Deleted";
|
||||
|
||||
"Monetization.StarsRevenueTitle" = "REVENUE";
|
||||
|
||||
"Monetization.TonBalanceTitle" = "TON BALANCE";
|
||||
"Monetization.StarsBalanceTitle" = "STARS BALANCE";
|
||||
|
||||
"Monetization.TonTransactions" = "TON Transactions";
|
||||
"Monetization.StarsTransactions" = "Stars Transactions";
|
||||
|
||||
"Monetization.BalanceStarsWithdraw" = "Withdraw via Fragment";
|
||||
"Monetization.Balance.StarsInfo" = "You can withdraw Stars using Fragment, or use Stars to advertise your channel. [Learn More >]()";
|
||||
"Monetization.Balance.StarsInfo_URL" = "https://telegram.org";
|
||||
|
||||
"Premium.MessageEffects" = "Message Effects";
|
||||
"Premium.MessageEffectsInfo" = "Add over 500 animated effects to private messages.";
|
||||
|
|
|
|||
|
|
@ -1065,11 +1065,12 @@ public protocol SharedAccountContext: AnyObject {
|
|||
|
||||
func makeStarsTransactionsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController
|
||||
func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, requiredStars: Int64?, completion: @escaping (Int64) -> Void) -> ViewController
|
||||
func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController
|
||||
func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, isAccount: Bool) -> ViewController
|
||||
func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController
|
||||
func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, peer: EnginePeer) -> ViewController
|
||||
func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController
|
||||
func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController
|
||||
func makeStarsAmountScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController
|
||||
func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController
|
||||
func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController
|
||||
|
||||
func makeDebugSettingsController(context: AccountContext?) -> ViewController?
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import TelegramCore
|
|||
|
||||
public enum GalleryControllerItemSource {
|
||||
case peerMessagesAtId(messageId: MessageId, chatLocation: ChatLocation, customTag: MemoryBuffer?, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>)
|
||||
case standaloneMessage(Message)
|
||||
case standaloneMessage(Message, Int?)
|
||||
case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId, loadMore: (() -> Void)?)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ public final class OpenChatMessageParams {
|
|||
public let chatFilterTag: MemoryBuffer?
|
||||
public let chatLocationContextHolder: Atomic<ChatLocationContextHolder?>?
|
||||
public let message: Message
|
||||
public let mediaIndex: Int?
|
||||
public let standalone: Bool
|
||||
public let reverseMessageGalleryOrder: Bool
|
||||
public let mode: ChatControllerInteractionOpenMessageMode
|
||||
|
|
@ -55,6 +56,7 @@ public final class OpenChatMessageParams {
|
|||
chatFilterTag: MemoryBuffer?,
|
||||
chatLocationContextHolder: Atomic<ChatLocationContextHolder?>?,
|
||||
message: Message,
|
||||
mediaIndex: Int? = nil,
|
||||
standalone: Bool,
|
||||
reverseMessageGalleryOrder: Bool,
|
||||
mode: ChatControllerInteractionOpenMessageMode = .default,
|
||||
|
|
@ -84,6 +86,7 @@ public final class OpenChatMessageParams {
|
|||
self.chatFilterTag = chatFilterTag
|
||||
self.chatLocationContextHolder = chatLocationContextHolder
|
||||
self.message = message
|
||||
self.mediaIndex = mediaIndex
|
||||
self.standalone = standalone
|
||||
self.reverseMessageGalleryOrder = reverseMessageGalleryOrder
|
||||
self.mode = mode
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ public enum PremiumIntroSource {
|
|||
case messageTags
|
||||
case folderTags
|
||||
case animatedEmoji
|
||||
case messageEffects
|
||||
}
|
||||
|
||||
public enum PremiumGiftSource: Equatable {
|
||||
|
|
@ -75,6 +76,7 @@ public enum PremiumDemoSubject {
|
|||
case messagePrivacy
|
||||
case folderTags
|
||||
case business
|
||||
case messageEffects
|
||||
|
||||
case businessLocation
|
||||
case businessHours
|
||||
|
|
|
|||
|
|
@ -859,6 +859,9 @@ private final class ChatListMediaPreviewNode: ASDisplayNode {
|
|||
let signal = mediaGridMessagePhoto(account: self.context.account, userLocation: .peer(self.message.id.peerId), photoReference: .message(message: MessageReference(self.message._asMessage()), media: image), fullRepresentationSize: CGSize(width: 36.0, height: 36.0), blurred: hasSpoiler, synchronousLoad: synchronousLoads)
|
||||
self.imageNode.setSignal(signal, attemptSynchronously: synchronousLoads)
|
||||
}
|
||||
} else {
|
||||
let signal = chatSecretPhoto(account: self.context.account, userLocation: .peer(self.message.id.peerId), photoReference: .standalone(media: image), ignoreFullSize: true, synchronousLoad: synchronousLoads)
|
||||
self.imageNode.setSignal(signal, attemptSynchronously: synchronousLoads)
|
||||
}
|
||||
} else if case let .action(action) = self.media, case let .suggestedProfilePhoto(image) = action.action, let image = image {
|
||||
isRound = true
|
||||
|
|
@ -2565,7 +2568,36 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
}
|
||||
|
||||
inner: for media in message.media {
|
||||
if let image = media as? TelegramMediaImage {
|
||||
if let paidContent = media as? TelegramMediaPaidContent {
|
||||
let fitSize = contentImageSize
|
||||
var index: Int64 = 0
|
||||
for media in paidContent.extendedMedia.prefix(3) {
|
||||
switch media {
|
||||
case let .preview(dimensions, immediateThumbnailData, videoDuration):
|
||||
if let immediateThumbnailData {
|
||||
if let videoDuration {
|
||||
let thumbnailMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: index), partialReference: nil, resource: EmptyMediaResource(), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Video(duration: Double(videoDuration), size: dimensions ?? PixelDimensions(width: 1, height: 1), flags: [], preloadSize: nil)])
|
||||
contentImageSpecs.append(ContentImageSpec(message: message, media: .file(thumbnailMedia), size: fitSize))
|
||||
} else {
|
||||
let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: index), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
||||
contentImageSpecs.append(ContentImageSpec(message: message, media: .image(thumbnailMedia), size: fitSize))
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
case let .full(fullMedia):
|
||||
if let image = fullMedia as? TelegramMediaImage {
|
||||
if let _ = largestImageRepresentation(image.representations) {
|
||||
contentImageSpecs.append(ContentImageSpec(message: message, media: .image(image), size: fitSize))
|
||||
}
|
||||
} else if let file = fullMedia as? TelegramMediaFile {
|
||||
if file.isVideo, !file.isVideoSticker, let _ = file.dimensions {
|
||||
contentImageSpecs.append(ContentImageSpec(message: message, media: .file(file), size: fitSize))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break inner
|
||||
} else if let image = media as? TelegramMediaImage {
|
||||
if let _ = largestImageRepresentation(image.representations) {
|
||||
let fitSize = contentImageSize
|
||||
contentImageSpecs.append(ContentImageSpec(message: message, media: .image(image), size: fitSize))
|
||||
|
|
|
|||
|
|
@ -31,6 +31,24 @@ private func singleMessageType(message: EngineMessage) -> MessageGroupType {
|
|||
return .generic
|
||||
}
|
||||
|
||||
private func singleExtendedMediaType(extendedMedia: TelegramExtendedMedia) -> MessageGroupType {
|
||||
switch extendedMedia {
|
||||
case let .preview(_, _, videoDuration):
|
||||
if let _ = videoDuration {
|
||||
return .videos
|
||||
} else {
|
||||
return .photos
|
||||
}
|
||||
case let .full(fullMedia):
|
||||
if let _ = fullMedia as? TelegramMediaImage {
|
||||
return .photos
|
||||
} else if let file = fullMedia as? TelegramMediaFile, file.isVideo {
|
||||
return .videos
|
||||
}
|
||||
}
|
||||
return .generic
|
||||
}
|
||||
|
||||
private func messageGroupType(messages: [EngineMessage]) -> MessageGroupType {
|
||||
if messages.isEmpty {
|
||||
return .generic
|
||||
|
|
@ -45,6 +63,20 @@ private func messageGroupType(messages: [EngineMessage]) -> MessageGroupType {
|
|||
return currentType
|
||||
}
|
||||
|
||||
private func paidContentGroupType(paidContent: TelegramMediaPaidContent) -> MessageGroupType {
|
||||
if paidContent.extendedMedia.isEmpty {
|
||||
return .generic
|
||||
}
|
||||
let currentType = singleExtendedMediaType(extendedMedia: paidContent.extendedMedia[0])
|
||||
for i in 1 ..< paidContent.extendedMedia.count {
|
||||
let nextType = singleExtendedMediaType(extendedMedia: paidContent.extendedMedia[i])
|
||||
if nextType != currentType {
|
||||
return .generic
|
||||
}
|
||||
}
|
||||
return currentType
|
||||
}
|
||||
|
||||
public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, contentSettings: ContentSettings, messages: [EngineMessage], chatPeer: EngineRenderedPeer, accountPeerId: EnginePeer.Id, enableMediaEmoji: Bool = true, isPeerGroup: Bool = false) -> (peer: EnginePeer?, hideAuthor: Bool, messageText: String, spoilers: [NSRange]?, customEmojiRanges: [(NSRange, ChatTextInputTextCustomEmojiAttribute)]?) {
|
||||
let peer: EnginePeer?
|
||||
|
||||
|
|
@ -76,42 +108,59 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
let paidContent = message.media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent
|
||||
|
||||
var textIsReady = false
|
||||
if messages.count > 1 {
|
||||
let groupType = messageGroupType(messages: messages)
|
||||
if messages.count > 1 || (paidContent != nil && (paidContent?.extendedMedia.count ?? 0) > 1) {
|
||||
let groupType: MessageGroupType
|
||||
let count: Int32
|
||||
if let paidContent {
|
||||
groupType = paidContentGroupType(paidContent: paidContent)
|
||||
count = Int32(paidContent.extendedMedia.count)
|
||||
} else {
|
||||
groupType = messageGroupType(messages: messages)
|
||||
count = Int32(messages.count)
|
||||
}
|
||||
switch groupType {
|
||||
case .photos:
|
||||
if !messageText.isEmpty {
|
||||
textIsReady = true
|
||||
} else {
|
||||
messageText = strings.ChatList_MessagePhotos(Int32(messages.count))
|
||||
messageText = strings.ChatList_MessagePhotos(count)
|
||||
textIsReady = true
|
||||
}
|
||||
case .videos:
|
||||
if !messageText.isEmpty {
|
||||
textIsReady = true
|
||||
} else {
|
||||
messageText = strings.ChatList_MessageVideos(Int32(messages.count))
|
||||
messageText = strings.ChatList_MessageVideos(count)
|
||||
textIsReady = true
|
||||
}
|
||||
case .music:
|
||||
if !messageText.isEmpty {
|
||||
textIsReady = true
|
||||
} else {
|
||||
messageText = strings.ChatList_MessageMusic(Int32(messages.count))
|
||||
messageText = strings.ChatList_MessageMusic(count)
|
||||
textIsReady = true
|
||||
}
|
||||
case .files:
|
||||
if !messageText.isEmpty {
|
||||
textIsReady = true
|
||||
} else {
|
||||
messageText = strings.ChatList_MessageFiles(Int32(messages.count))
|
||||
messageText = strings.ChatList_MessageFiles(count)
|
||||
textIsReady = true
|
||||
}
|
||||
case .generic:
|
||||
var messageTypes = Set<MessageGroupType>()
|
||||
for message in messages {
|
||||
messageTypes.insert(singleMessageType(message: message))
|
||||
if let paidContent {
|
||||
for extendedMedia in paidContent.extendedMedia {
|
||||
messageTypes.insert(singleExtendedMediaType(extendedMedia: extendedMedia))
|
||||
}
|
||||
} else {
|
||||
for message in messages {
|
||||
messageTypes.insert(singleMessageType(message: message))
|
||||
}
|
||||
}
|
||||
if messageTypes.count == 2 && messageTypes.contains(.photos) && messageTypes.contains(.videos) {
|
||||
if !messageText.isEmpty {
|
||||
|
|
@ -124,6 +173,26 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
|||
if !textIsReady {
|
||||
for media in message.media {
|
||||
switch media {
|
||||
case let paidContent as TelegramMediaPaidContent:
|
||||
for extendedMedia in paidContent.extendedMedia {
|
||||
let type = singleExtendedMediaType(extendedMedia: extendedMedia)
|
||||
switch type {
|
||||
case .photos:
|
||||
if message.text.isEmpty {
|
||||
messageText = strings.Message_Photo
|
||||
} else if enableMediaEmoji {
|
||||
messageText = "🖼 \(messageText)"
|
||||
}
|
||||
case .videos:
|
||||
if message.text.isEmpty {
|
||||
messageText = strings.Message_Video
|
||||
} else if enableMediaEmoji {
|
||||
messageText = "📹 \(messageText)"
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
case _ as TelegramMediaImage:
|
||||
if message.text.isEmpty {
|
||||
messageText = strings.Message_Photo
|
||||
|
|
@ -337,12 +406,6 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
|||
if messageText.isEmpty, case let .Loaded(content) = webpage.content {
|
||||
messageText = content.displayUrl
|
||||
}
|
||||
case _ as TelegramMediaPaidContent:
|
||||
if message.text.isEmpty {
|
||||
messageText = "Paid Media"
|
||||
} else {
|
||||
messageText = "🖼 \(messageText)"
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ public func instantPageGalleryMedia(webpageId: MediaId, page: InstantPage, galle
|
|||
return result
|
||||
}
|
||||
|
||||
public func chatMessageGalleryControllerData(context: AccountContext, chatLocation: ChatLocation?, chatFilterTag: MemoryBuffer?, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>?, message: Message, navigationController: NavigationController?, standalone: Bool, reverseMessageGalleryOrder: Bool, mode: ChatControllerInteractionOpenMessageMode, source: GalleryControllerItemSource?, synchronousLoad: Bool, actionInteraction: GalleryControllerActionInteraction?) -> ChatMessageGalleryControllerData? {
|
||||
public func chatMessageGalleryControllerData(context: AccountContext, chatLocation: ChatLocation?, chatFilterTag: MemoryBuffer?, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>?, message: Message, mediaIndex: Int? = nil, navigationController: NavigationController?, standalone: Bool, reverseMessageGalleryOrder: Bool, mode: ChatControllerInteractionOpenMessageMode, source: GalleryControllerItemSource?, synchronousLoad: Bool, actionInteraction: GalleryControllerActionInteraction?) -> ChatMessageGalleryControllerData? {
|
||||
var standalone = standalone
|
||||
if message.id.peerId.namespace == Namespaces.Peer.CloudUser && message.id.namespace != Namespaces.Message.Cloud {
|
||||
standalone = true
|
||||
|
|
@ -111,9 +111,9 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
|
|||
}
|
||||
}
|
||||
for media in message.media {
|
||||
if let paidContent = media as? TelegramMediaPaidContent, let extendedMedia = paidContent.extendedMedia.first, case let .full(fullMedia) = extendedMedia {
|
||||
if let paidContent = media as? TelegramMediaPaidContent, let extendedMedia = paidContent.extendedMedia.first, case .full = extendedMedia {
|
||||
standalone = true
|
||||
galleryMedia = fullMedia
|
||||
galleryMedia = paidContent
|
||||
} else if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia {
|
||||
standalone = true
|
||||
galleryMedia = fullMedia
|
||||
|
|
@ -241,7 +241,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
|
|||
|
||||
var source = source
|
||||
if standalone {
|
||||
source = .standaloneMessage(message)
|
||||
source = .standaloneMessage(message, nil)
|
||||
}
|
||||
|
||||
if internalDocumentItemSupportsMimeType(file.mimeType, fileName: file.fileName ?? "file") {
|
||||
|
|
@ -280,7 +280,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
|
|||
return .gallery(startState
|
||||
|> deliverOnMainQueue
|
||||
|> map { startState in
|
||||
let gallery = GalleryController(context: context, source: source ?? (standalone ? .standaloneMessage(message) : .peerMessagesAtId(messageId: message.id, chatLocation: openChatLocation, customTag: chatFilterTag, chatLocationContextHolder: openChatLocationContextHolder)), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: startState.timecode, playbackRate: startState.rate, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in
|
||||
let gallery = GalleryController(context: context, source: source ?? (standalone ? .standaloneMessage(message, mediaIndex) : .peerMessagesAtId(messageId: message.id, chatLocation: openChatLocation, customTag: chatFilterTag, chatLocationContextHolder: openChatLocationContextHolder)), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: startState.timecode, playbackRate: startState.rate, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in
|
||||
navigationController?.replaceTopController(controller, animated: false, ready: ready)
|
||||
}, baseNavigationController: navigationController, actionInteraction: actionInteraction)
|
||||
gallery.temporaryDoNotWaitForReady = autoplayingVideo
|
||||
|
|
|
|||
|
|
@ -903,7 +903,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
|
|||
canEdit = false
|
||||
}
|
||||
|
||||
if message.isCopyProtected() || peerIsCopyProtected {
|
||||
if message.isCopyProtected() || peerIsCopyProtected || message.paidContent != nil {
|
||||
canShare = false
|
||||
canEdit = false
|
||||
}
|
||||
|
|
@ -931,7 +931,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
|
|||
var messageText = NSAttributedString(string: "")
|
||||
var hasCaption = false
|
||||
for media in message.media {
|
||||
if media is TelegramMediaImage {
|
||||
if media is TelegramMediaPaidContent {
|
||||
hasCaption = true
|
||||
} else if media is TelegramMediaImage {
|
||||
hasCaption = true
|
||||
} else if let file = media as? TelegramMediaFile {
|
||||
hasCaption = file.mimeType.hasPrefix("image/")
|
||||
|
|
|
|||
|
|
@ -44,9 +44,7 @@ private func tagsForMessage(_ message: Message) -> MessageTags? {
|
|||
}
|
||||
|
||||
private func galleryMediaForMedia(media: Media) -> Media? {
|
||||
if let paidContent = media as? TelegramMediaPaidContent, let extendedMedia = paidContent.extendedMedia.first, case let .full(fullMedia) = extendedMedia {
|
||||
return fullMedia
|
||||
} else if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia {
|
||||
if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia {
|
||||
return fullMedia
|
||||
} else if let media = media as? TelegramMediaImage {
|
||||
return media
|
||||
|
|
@ -62,22 +60,30 @@ private func galleryMediaForMedia(media: Media) -> Media? {
|
|||
return nil
|
||||
}
|
||||
|
||||
private func mediaForMessage(message: Message) -> (Media, TelegramMediaImage?)? {
|
||||
private func mediaForMessage(message: Message) -> [(Media, TelegramMediaImage?)] {
|
||||
for media in message.media {
|
||||
if let result = galleryMediaForMedia(media: media) {
|
||||
return (result, nil)
|
||||
return [(result, nil)]
|
||||
} else if let paidContent = media as? TelegramMediaPaidContent {
|
||||
var results: [(Media, TelegramMediaImage?)] = []
|
||||
for case let .full(fullMedia) in paidContent.extendedMedia {
|
||||
if let result = galleryMediaForMedia(media: fullMedia) {
|
||||
results.append((result, nil))
|
||||
}
|
||||
}
|
||||
return results
|
||||
} else if let webpage = media as? TelegramMediaWebpage {
|
||||
switch webpage.content {
|
||||
case let .Loaded(content):
|
||||
if let embedUrl = content.embedUrl, !embedUrl.isEmpty {
|
||||
return (webpage, nil)
|
||||
return [(webpage, nil)]
|
||||
} else if let file = content.file {
|
||||
if let result = galleryMediaForMedia(media: file) {
|
||||
return (result, content.image)
|
||||
return [(result, content.image)]
|
||||
}
|
||||
} else if let image = content.image {
|
||||
if let result = galleryMediaForMedia(media: image) {
|
||||
return (result, nil)
|
||||
return [(result, nil)]
|
||||
}
|
||||
}
|
||||
case .Pending:
|
||||
|
|
@ -85,7 +91,7 @@ private func mediaForMessage(message: Message) -> (Media, TelegramMediaImage?)?
|
|||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return []
|
||||
}
|
||||
|
||||
private let internalExtensions = Set<String>([
|
||||
|
|
@ -176,7 +182,7 @@ private func galleryMessageCaptionText(_ message: Message) -> String {
|
|||
public func galleryItemForEntry(
|
||||
context: AccountContext,
|
||||
presentationData: PresentationData,
|
||||
entry: MessageHistoryEntry,
|
||||
entry: GalleryEntry,
|
||||
isCentral: Bool = false,
|
||||
streamVideos: Bool,
|
||||
loopVideos: Bool = false,
|
||||
|
|
@ -198,195 +204,209 @@ public func galleryItemForEntry(
|
|||
generateStoreAfterDownload: ((Message, TelegramMediaFile) -> (() -> Void)?)? = nil,
|
||||
present: @escaping (ViewController, Any?) -> Void) -> GalleryItem?
|
||||
{
|
||||
let message = entry.message
|
||||
let location = entry.location
|
||||
if let (media, mediaImage) = mediaForMessage(message: message) {
|
||||
if let _ = media as? TelegramMediaImage {
|
||||
return ChatImageGalleryItem(
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
message: message,
|
||||
location: location,
|
||||
translateToLanguage: translateToLanguage,
|
||||
peerIsCopyProtected: peerIsCopyProtected,
|
||||
isSecret: isSecret,
|
||||
displayInfoOnTop: displayInfoOnTop,
|
||||
performAction: performAction,
|
||||
openActionOptions: openActionOptions,
|
||||
present: present
|
||||
)
|
||||
} else if let file = media as? TelegramMediaFile {
|
||||
if file.isVideo {
|
||||
let content: UniversalVideoContent
|
||||
let captureProtected = message.isCopyProtected() || message.containsSecretMedia || message.minAutoremoveOrClearTimeout == viewOnceTimeout
|
||||
if file.isAnimated {
|
||||
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), loopVideo: true, enableSound: false, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
|
||||
let message = entry.entry.message
|
||||
let location = entry.location ?? entry.entry.location
|
||||
let messageMedia = mediaForMessage(message: message)
|
||||
|
||||
let mediaAndMediaImage: (Media, TelegramMediaImage?)?
|
||||
if let mediaIndex = entry.mediaIndex {
|
||||
if mediaIndex < messageMedia.count {
|
||||
mediaAndMediaImage = messageMedia[Int(mediaIndex)]
|
||||
} else {
|
||||
mediaAndMediaImage = nil
|
||||
}
|
||||
} else {
|
||||
mediaAndMediaImage = messageMedia.first
|
||||
}
|
||||
guard let (media, mediaImage) = mediaAndMediaImage else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let _ = media as? TelegramMediaImage {
|
||||
return ChatImageGalleryItem(
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
message: message,
|
||||
mediaIndex: entry.mediaIndex,
|
||||
location: location,
|
||||
translateToLanguage: translateToLanguage,
|
||||
peerIsCopyProtected: peerIsCopyProtected,
|
||||
isSecret: isSecret,
|
||||
displayInfoOnTop: displayInfoOnTop,
|
||||
performAction: performAction,
|
||||
openActionOptions: openActionOptions,
|
||||
present: present
|
||||
)
|
||||
} else if let file = media as? TelegramMediaFile {
|
||||
if file.isVideo {
|
||||
let content: UniversalVideoContent
|
||||
let captureProtected = message.isCopyProtected() || message.containsSecretMedia || message.minAutoremoveOrClearTimeout == viewOnceTimeout
|
||||
if file.isAnimated {
|
||||
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), loopVideo: true, enableSound: false, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
|
||||
} else {
|
||||
if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") {
|
||||
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
|
||||
} else {
|
||||
if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") {
|
||||
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
|
||||
} else {
|
||||
content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), content: .file(.message(message: MessageReference(message), media: file)), streamVideo: streamVideos, loopVideo: loopVideos)
|
||||
}
|
||||
content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), content: .file(.message(message: MessageReference(message), media: file)), streamVideo: streamVideos, loopVideo: loopVideos)
|
||||
}
|
||||
|
||||
var entities: [MessageTextEntity] = []
|
||||
}
|
||||
|
||||
var entities: [MessageTextEntity] = []
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
||||
entities = attribute.entities
|
||||
break
|
||||
}
|
||||
}
|
||||
var text = galleryMessageCaptionText(message)
|
||||
if let translateToLanguage, !text.isEmpty {
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
||||
if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage {
|
||||
text = attribute.text
|
||||
entities = attribute.entities
|
||||
break
|
||||
}
|
||||
}
|
||||
var text = galleryMessageCaptionText(message)
|
||||
if let translateToLanguage, !text.isEmpty {
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage {
|
||||
text = attribute.text
|
||||
entities = attribute.entities
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let result = addLocallyGeneratedEntities(text, enabledTypes: [.timecode], entities: entities, mediaDuration: file.duration.flatMap(Double.init)) {
|
||||
entities = result
|
||||
}
|
||||
|
||||
var originData = GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp)
|
||||
if Namespaces.Message.allNonRegular.contains(message.id.namespace) {
|
||||
originData = GalleryItemOriginData(title: nil, timestamp: nil)
|
||||
}
|
||||
|
||||
let caption = galleryCaptionStringWithAppliedEntities(context: context, text: text, entities: entities, message: message)
|
||||
return UniversalVideoGalleryItem(
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
content: content,
|
||||
originData: originData,
|
||||
indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) },
|
||||
contentInfo: .message(message),
|
||||
caption: caption,
|
||||
displayInfoOnTop: displayInfoOnTop,
|
||||
hideControls: hideControls,
|
||||
fromPlayingVideo: fromPlayingVideo,
|
||||
isSecret: isSecret,
|
||||
landscape: landscape,
|
||||
timecode: timecode,
|
||||
peerIsCopyProtected: peerIsCopyProtected,
|
||||
playbackRate: playbackRate,
|
||||
configuration: configuration,
|
||||
playbackCompleted: playbackCompleted,
|
||||
performAction: performAction,
|
||||
openActionOptions: openActionOptions,
|
||||
storeMediaPlaybackState: storeMediaPlaybackState,
|
||||
present: present
|
||||
)
|
||||
} else {
|
||||
if let fileName = file.fileName, (fileName as NSString).pathExtension.lowercased() == "json" {
|
||||
return ChatAnimationGalleryItem(context: context, presentationData: presentationData, message: message, location: location)
|
||||
}
|
||||
else if file.mimeType.hasPrefix("image/") && file.mimeType != "image/gif" {
|
||||
var pixelsCount: Int = 0
|
||||
if let dimensions = file.dimensions {
|
||||
pixelsCount = Int(dimensions.width) * Int(dimensions.height)
|
||||
}
|
||||
|
||||
if let result = addLocallyGeneratedEntities(text, enabledTypes: [.timecode], entities: entities, mediaDuration: file.duration.flatMap(Double.init)) {
|
||||
entities = result
|
||||
}
|
||||
|
||||
var originData = GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp)
|
||||
if Namespaces.Message.allNonRegular.contains(message.id.namespace) {
|
||||
originData = GalleryItemOriginData(title: nil, timestamp: nil)
|
||||
}
|
||||
|
||||
let caption = galleryCaptionStringWithAppliedEntities(context: context, text: text, entities: entities, message: message)
|
||||
return UniversalVideoGalleryItem(
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
content: content,
|
||||
originData: originData,
|
||||
indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) },
|
||||
contentInfo: .message(message),
|
||||
caption: caption,
|
||||
displayInfoOnTop: displayInfoOnTop,
|
||||
hideControls: hideControls,
|
||||
fromPlayingVideo: fromPlayingVideo,
|
||||
isSecret: isSecret,
|
||||
landscape: landscape,
|
||||
timecode: timecode,
|
||||
peerIsCopyProtected: peerIsCopyProtected,
|
||||
playbackRate: playbackRate,
|
||||
configuration: configuration,
|
||||
playbackCompleted: playbackCompleted,
|
||||
performAction: performAction,
|
||||
openActionOptions: openActionOptions,
|
||||
storeMediaPlaybackState: storeMediaPlaybackState,
|
||||
present: present
|
||||
)
|
||||
} else {
|
||||
if let fileName = file.fileName, (fileName as NSString).pathExtension.lowercased() == "json" {
|
||||
return ChatAnimationGalleryItem(context: context, presentationData: presentationData, message: message, location: location)
|
||||
}
|
||||
else if file.mimeType.hasPrefix("image/") && file.mimeType != "image/gif" {
|
||||
var pixelsCount: Int = 0
|
||||
if let dimensions = file.dimensions {
|
||||
pixelsCount = Int(dimensions.width) * Int(dimensions.height)
|
||||
}
|
||||
if pixelsCount < 10000 * 10000 {
|
||||
return ChatImageGalleryItem(
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
message: message,
|
||||
location: location,
|
||||
translateToLanguage: translateToLanguage,
|
||||
peerIsCopyProtected: peerIsCopyProtected,
|
||||
isSecret: isSecret,
|
||||
displayInfoOnTop: displayInfoOnTop,
|
||||
performAction: performAction,
|
||||
openActionOptions: openActionOptions,
|
||||
present: present
|
||||
)
|
||||
} else {
|
||||
return ChatDocumentGalleryItem(
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
message: message,
|
||||
location: location
|
||||
)
|
||||
}
|
||||
} else if internalDocumentItemSupportsMimeType(file.mimeType, fileName: file.fileName) {
|
||||
if pixelsCount < 10000 * 10000 {
|
||||
return ChatImageGalleryItem(
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
message: message,
|
||||
location: location,
|
||||
translateToLanguage: translateToLanguage,
|
||||
peerIsCopyProtected: peerIsCopyProtected,
|
||||
isSecret: isSecret,
|
||||
displayInfoOnTop: displayInfoOnTop,
|
||||
performAction: performAction,
|
||||
openActionOptions: openActionOptions,
|
||||
present: present
|
||||
)
|
||||
} else {
|
||||
return ChatDocumentGalleryItem(
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
message: message,
|
||||
location: location
|
||||
)
|
||||
} else {
|
||||
return ChatExternalFileGalleryItem(
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
message: message,
|
||||
location: location
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(webpageContent) = webpage.content {
|
||||
var content: UniversalVideoContent?
|
||||
switch websiteType(of: webpageContent.websiteName) {
|
||||
case .instagram where webpageContent.file != nil && webpageContent.image != nil && webpageContent.file!.isVideo:
|
||||
content = NativeVideoContent(id: .message(message.stableId, webpageContent.file?.id ?? webpage.webpageId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: webpageContent.file!), imageReference: webpageContent.image.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, enableSound: true, captureProtected: message.isCopyProtected() || message.containsSecretMedia, storeAfterDownload: nil)
|
||||
default:
|
||||
if let embedUrl = webpageContent.embedUrl, let image = webpageContent.image {
|
||||
if let file = webpageContent.file, file.isVideo {
|
||||
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: message.isCopyProtected() || message.containsSecretMedia, storeAfterDownload: generateStoreAfterDownload?(message, file))
|
||||
} else if URL(string: embedUrl)?.pathExtension == "mp4" {
|
||||
content = SystemVideoContent(userLocation: .peer(message.id.peerId), url: embedUrl, imageReference: .webPage(webPage: WebpageReference(webpage), media: image), dimensions: webpageContent.embedSize?.cgSize ?? CGSize(width: 640.0, height: 640.0), duration: webpageContent.duration.flatMap(Double.init) ?? 0.0)
|
||||
}
|
||||
}
|
||||
if content == nil, let webEmbedContent = WebEmbedVideoContent(userLocation: .peer(message.id.peerId), webPage: webpage, webpageContent: webpageContent, forcedTimestamp: timecode.flatMap(Int.init), openUrl: { url in
|
||||
performAction(.url(url: url.absoluteString, concealed: false))
|
||||
}) {
|
||||
content = webEmbedContent
|
||||
}
|
||||
}
|
||||
if let content = content {
|
||||
var description: NSAttributedString?
|
||||
if let descriptionText = webpageContent.text {
|
||||
var entities: [MessageTextEntity] = []
|
||||
if let result = addLocallyGeneratedEntities(descriptionText, enabledTypes: [.timecode], entities: entities, mediaDuration: 86400) {
|
||||
entities = result
|
||||
}
|
||||
description = galleryCaptionStringWithAppliedEntities(context: context, text: descriptionText, entities: entities, message: message)
|
||||
}
|
||||
|
||||
var originData = GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp)
|
||||
if Namespaces.Message.allNonRegular.contains(message.id.namespace) {
|
||||
originData = GalleryItemOriginData(title: nil, timestamp: nil)
|
||||
}
|
||||
|
||||
return UniversalVideoGalleryItem(
|
||||
} else if internalDocumentItemSupportsMimeType(file.mimeType, fileName: file.fileName) {
|
||||
return ChatDocumentGalleryItem(
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
content: content,
|
||||
originData: originData,
|
||||
indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) },
|
||||
contentInfo: .message(message),
|
||||
caption: NSAttributedString(string: ""),
|
||||
description: description,
|
||||
displayInfoOnTop: displayInfoOnTop,
|
||||
fromPlayingVideo: fromPlayingVideo,
|
||||
isSecret: isSecret,
|
||||
landscape: landscape,
|
||||
timecode: timecode,
|
||||
playbackRate: playbackRate,
|
||||
configuration: configuration,
|
||||
performAction: performAction,
|
||||
openActionOptions: openActionOptions,
|
||||
storeMediaPlaybackState: storeMediaPlaybackState,
|
||||
present: present
|
||||
message: message,
|
||||
location: location
|
||||
)
|
||||
} else {
|
||||
return nil
|
||||
return ChatExternalFileGalleryItem(
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
message: message,
|
||||
location: location
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(webpageContent) = webpage.content {
|
||||
var content: UniversalVideoContent?
|
||||
switch websiteType(of: webpageContent.websiteName) {
|
||||
case .instagram where webpageContent.file != nil && webpageContent.image != nil && webpageContent.file!.isVideo:
|
||||
content = NativeVideoContent(id: .message(message.stableId, webpageContent.file?.id ?? webpage.webpageId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: webpageContent.file!), imageReference: webpageContent.image.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, enableSound: true, captureProtected: message.isCopyProtected() || message.containsSecretMedia, storeAfterDownload: nil)
|
||||
default:
|
||||
if let embedUrl = webpageContent.embedUrl, let image = webpageContent.image {
|
||||
if let file = webpageContent.file, file.isVideo {
|
||||
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: message.isCopyProtected() || message.containsSecretMedia, storeAfterDownload: generateStoreAfterDownload?(message, file))
|
||||
} else if URL(string: embedUrl)?.pathExtension == "mp4" {
|
||||
content = SystemVideoContent(userLocation: .peer(message.id.peerId), url: embedUrl, imageReference: .webPage(webPage: WebpageReference(webpage), media: image), dimensions: webpageContent.embedSize?.cgSize ?? CGSize(width: 640.0, height: 640.0), duration: webpageContent.duration.flatMap(Double.init) ?? 0.0)
|
||||
}
|
||||
}
|
||||
if content == nil, let webEmbedContent = WebEmbedVideoContent(userLocation: .peer(message.id.peerId), webPage: webpage, webpageContent: webpageContent, forcedTimestamp: timecode.flatMap(Int.init), openUrl: { url in
|
||||
performAction(.url(url: url.absoluteString, concealed: false))
|
||||
}) {
|
||||
content = webEmbedContent
|
||||
}
|
||||
}
|
||||
if let content = content {
|
||||
var description: NSAttributedString?
|
||||
if let descriptionText = webpageContent.text {
|
||||
var entities: [MessageTextEntity] = []
|
||||
if let result = addLocallyGeneratedEntities(descriptionText, enabledTypes: [.timecode], entities: entities, mediaDuration: 86400) {
|
||||
entities = result
|
||||
}
|
||||
description = galleryCaptionStringWithAppliedEntities(context: context, text: descriptionText, entities: entities, message: message)
|
||||
}
|
||||
|
||||
var originData = GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp)
|
||||
if Namespaces.Message.allNonRegular.contains(message.id.namespace) {
|
||||
originData = GalleryItemOriginData(title: nil, timestamp: nil)
|
||||
}
|
||||
|
||||
return UniversalVideoGalleryItem(
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
content: content,
|
||||
originData: originData,
|
||||
indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) },
|
||||
contentInfo: .message(message),
|
||||
caption: NSAttributedString(string: ""),
|
||||
description: description,
|
||||
displayInfoOnTop: displayInfoOnTop,
|
||||
fromPlayingVideo: fromPlayingVideo,
|
||||
isSecret: isSecret,
|
||||
landscape: landscape,
|
||||
timecode: timecode,
|
||||
playbackRate: playbackRate,
|
||||
configuration: configuration,
|
||||
performAction: performAction,
|
||||
openActionOptions: openActionOptions,
|
||||
storeMediaPlaybackState: storeMediaPlaybackState,
|
||||
present: present
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -493,6 +513,38 @@ public struct GalleryConfiguration {
|
|||
}
|
||||
}
|
||||
|
||||
public struct GalleryEntryStableId: Hashable {
|
||||
public var stableId: UInt32
|
||||
public var mediaIndex: Int?
|
||||
}
|
||||
|
||||
public struct GalleryEntry {
|
||||
public var entry: MessageHistoryEntry
|
||||
public var mediaIndex: Int?
|
||||
public var location: MessageHistoryEntryLocation?
|
||||
|
||||
public var stableId: GalleryEntryStableId {
|
||||
return GalleryEntryStableId(stableId: self.entry.message.stableId, mediaIndex: self.mediaIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private func galleryEntriesForMessageHistoryEntries(_ entries: [MessageHistoryEntry]) -> [GalleryEntry] {
|
||||
var results: [GalleryEntry] = []
|
||||
for entry in entries {
|
||||
let mediaCount = mediaForMessage(message: entry.message).count
|
||||
if mediaCount > 0 {
|
||||
if mediaCount > 1 {
|
||||
for i in 0 ..< mediaCount {
|
||||
results.append(GalleryEntry(entry: entry, mediaIndex: i, location: MessageHistoryEntryLocation(index: i, count: mediaCount)))
|
||||
}
|
||||
} else {
|
||||
results.append(GalleryEntry(entry: entry))
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
public class GalleryController: ViewController, StandalonePresentableController, KeyShortcutResponder {
|
||||
public static let darkNavigationTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), enableBackgroundBlur: false, separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
|
||||
public static let lightNavigationTheme = NavigationBarTheme(buttonColor: UIColor(rgb: 0x007aff), disabledButtonColor: UIColor(rgb: 0xd0d0d0), primaryTextColor: .black, backgroundColor: UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0), enableBackgroundBlur: false, separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
|
||||
|
|
@ -526,12 +578,12 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
private let disposable = MetaDisposable()
|
||||
|
||||
private var peerIsCopyProtected = false
|
||||
private var entries: [MessageHistoryEntry] = []
|
||||
private var entries: [GalleryEntry] = []
|
||||
private var hasLeftEntries: Bool = false
|
||||
private var hasRightEntries: Bool = false
|
||||
private var loadingMore: Bool = false
|
||||
private var tag: HistoryViewInputTag?
|
||||
private var centralEntryStableId: UInt32?
|
||||
private var centralEntryStableId: GalleryEntryStableId?
|
||||
private var configuration: GalleryConfiguration?
|
||||
|
||||
private let centralItemTitle = Promise<String>()
|
||||
|
|
@ -650,7 +702,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
return nil
|
||||
}
|
||||
}
|
||||
case let .standaloneMessage(m):
|
||||
case let .standaloneMessage(m, _):
|
||||
message = .single((m, m.isCopyProtected()))
|
||||
case let .custom(messages, messageId, _):
|
||||
message = messages
|
||||
|
|
@ -732,24 +784,25 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
let configuration = GalleryConfiguration.with(appConfiguration: appConfiguration)
|
||||
strongSelf.configuration = configuration
|
||||
|
||||
let entries = view.entries
|
||||
var centralEntryStableId: UInt32?
|
||||
let entries = galleryEntriesForMessageHistoryEntries(view.entries)
|
||||
var centralEntryStableId: GalleryEntryStableId?
|
||||
loop: for i in 0 ..< entries.count {
|
||||
let message = entries[i].message
|
||||
let entry = entries[i]
|
||||
let message = entry.entry.message
|
||||
switch source {
|
||||
case let .peerMessagesAtId(messageId, _, _, _):
|
||||
if message.id == messageId {
|
||||
centralEntryStableId = message.stableId
|
||||
centralEntryStableId = entry.stableId
|
||||
break loop
|
||||
}
|
||||
case let .standaloneMessage(m):
|
||||
if message.id == m.id {
|
||||
centralEntryStableId = message.stableId
|
||||
case let .standaloneMessage(m, mediaIndex):
|
||||
if message.id == m.id && entry.mediaIndex == mediaIndex {
|
||||
centralEntryStableId = entry.stableId
|
||||
break loop
|
||||
}
|
||||
case let .custom(_, messageId, _):
|
||||
if message.id == messageId {
|
||||
centralEntryStableId = message.stableId
|
||||
centralEntryStableId = entry.stableId
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
|
@ -775,7 +828,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
var centralItemIndex: Int?
|
||||
for entry in strongSelf.entries {
|
||||
var isCentral = false
|
||||
if entry.message.stableId == strongSelf.centralEntryStableId {
|
||||
if entry.stableId == strongSelf.centralEntryStableId {
|
||||
isCentral = true
|
||||
}
|
||||
if let item = galleryItemForEntry(context: context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: streamSingleVideo, fromPlayingVideo: isCentral && fromPlayingVideo, landscape: isCentral && landscape, timecode: isCentral ? timecode : nil, playbackRate: { return self?.playbackRate }, displayInfoOnTop: displayInfoOnTop, configuration: configuration, translateToLanguage: translateToLanguage, peerIsCopyProtected: view.peerIsCopyProtected, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, generateStoreAfterDownload: strongSelf.generateStoreAfterDownload, present: { [weak self] c, a in
|
||||
|
|
@ -1227,13 +1280,25 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
}
|
||||
|
||||
if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments {
|
||||
let message = self.entries[centralItemNode.index].message
|
||||
if let (media, _) = mediaForMessage(message: message), let transitionArguments = presentationArguments.transitionArguments(message.id, media), !forceAway {
|
||||
animatedOutNode = false
|
||||
centralItemNode.animateOut(to: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {
|
||||
animatedOutNode = true
|
||||
completion()
|
||||
})
|
||||
let entry = self.entries[centralItemNode.index]
|
||||
let message = entry.entry.message
|
||||
let media = mediaForMessage(message: message)
|
||||
if !media.isEmpty {
|
||||
var selectedMedia: Media?
|
||||
if let mediaIndex = entry.mediaIndex {
|
||||
if mediaIndex < media.count {
|
||||
selectedMedia = media[Int(mediaIndex)].0
|
||||
}
|
||||
} else if let media = media.first {
|
||||
selectedMedia = media.0
|
||||
}
|
||||
if let selectedMedia, let transitionArguments = presentationArguments.transitionArguments(message.id, selectedMedia), !forceAway {
|
||||
animatedOutNode = false
|
||||
centralItemNode.animateOut(to: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {
|
||||
animatedOutNode = true
|
||||
completion()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1287,9 +1352,22 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
self.galleryNode.transitionDataForCentralItem = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? GalleryControllerPresentationArguments {
|
||||
let message = strongSelf.entries[centralItemNode.index].message
|
||||
if let (media, _) = mediaForMessage(message: message), let transitionArguments = presentationArguments.transitionArguments(message.id, media) {
|
||||
return (transitionArguments.transitionNode, transitionArguments.addToTransitionSurface)
|
||||
let entry = strongSelf.entries[centralItemNode.index]
|
||||
let message = entry.entry.message
|
||||
let media = mediaForMessage(message: message)
|
||||
if !media.isEmpty {
|
||||
var selectedMedia: Media?
|
||||
if let mediaIndex = entry.mediaIndex {
|
||||
if mediaIndex < media.count {
|
||||
selectedMedia = media[Int(mediaIndex)].0
|
||||
}
|
||||
} else if let media = media.first {
|
||||
selectedMedia = media.0
|
||||
}
|
||||
|
||||
if let selectedMedia, let transitionArguments = presentationArguments.transitionArguments(message.id, selectedMedia) {
|
||||
return (transitionArguments.transitionNode, transitionArguments.addToTransitionSurface)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1355,7 +1433,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
var centralItemIndex: Int?
|
||||
for entry in self.entries {
|
||||
var isCentral = false
|
||||
if entry.message.stableId == self.centralEntryStableId {
|
||||
if entry.stableId == self.centralEntryStableId {
|
||||
isCentral = true
|
||||
}
|
||||
if let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: entry, streamVideos: self.streamVideos, fromPlayingVideo: isCentral && self.fromPlayingVideo, landscape: isCentral && self.landscape, timecode: isCentral ? self.timecode : nil, playbackRate: { [weak self] in return self?.playbackRate }, displayInfoOnTop: displayInfoOnTop, configuration: self.configuration, peerIsCopyProtected: self.peerIsCopyProtected, performAction: self.performAction, openActionOptions: self.openActionOptions, storeMediaPlaybackState: self.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, generateStoreAfterDownload: self.generateStoreAfterDownload, present: { [weak self] c, a in
|
||||
|
|
@ -1376,11 +1454,19 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
if let strongSelf = self {
|
||||
var hiddenItem: (MessageId, Media)?
|
||||
if let index = index {
|
||||
let message = strongSelf.entries[index].message
|
||||
let entry = strongSelf.entries[index]
|
||||
let message = strongSelf.entries[index].entry.message
|
||||
|
||||
strongSelf.centralEntryStableId = message.stableId
|
||||
if let (media, _) = mediaForMessage(message: message) {
|
||||
hiddenItem = (message.id, media)
|
||||
strongSelf.centralEntryStableId = entry.stableId
|
||||
let media = mediaForMessage(message: message)
|
||||
if !media.isEmpty {
|
||||
if let mediaIndex = entry.mediaIndex {
|
||||
if mediaIndex < media.count {
|
||||
hiddenItem = (message.id, media[Int(mediaIndex)].0)
|
||||
}
|
||||
} else if let media = media.first {
|
||||
hiddenItem = (message.id, media.0)
|
||||
}
|
||||
}
|
||||
|
||||
if let node = strongSelf.galleryNode.pager.centralItemNode() {
|
||||
|
|
@ -1397,9 +1483,9 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
case let .peerMessagesAtId(_, chatLocation, _, chatLocationContextHolder):
|
||||
var reloadAroundIndex: MessageIndex?
|
||||
if index <= 2 && strongSelf.hasLeftEntries {
|
||||
reloadAroundIndex = strongSelf.entries.first?.index
|
||||
reloadAroundIndex = strongSelf.entries.first?.entry.index
|
||||
} else if index >= strongSelf.entries.count - 3 && strongSelf.hasRightEntries {
|
||||
reloadAroundIndex = strongSelf.entries.last?.index
|
||||
reloadAroundIndex = strongSelf.entries.last?.entry.index
|
||||
}
|
||||
let peerIsCopyProtected = strongSelf.peerIsCopyProtected
|
||||
if let reloadAroundIndex = reloadAroundIndex, let tag = strongSelf.tag {
|
||||
|
|
@ -1424,8 +1510,8 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
return
|
||||
}
|
||||
|
||||
let entries = view.entries
|
||||
|
||||
let entries = galleryEntriesForMessageHistoryEntries(view.entries)
|
||||
|
||||
if strongSelf.invertItemOrder {
|
||||
strongSelf.entries = entries.reversed()
|
||||
strongSelf.hasLeftEntries = view.hasLater
|
||||
|
|
@ -1440,7 +1526,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
var centralItemIndex: Int?
|
||||
for entry in strongSelf.entries {
|
||||
var isCentral = false
|
||||
if entry.message.stableId == strongSelf.centralEntryStableId {
|
||||
if entry.stableId == strongSelf.centralEntryStableId {
|
||||
isCentral = true
|
||||
}
|
||||
if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, playbackRate: { return self?.playbackRate }, displayInfoOnTop: displayInfoOnTop, configuration: strongSelf.configuration, peerIsCopyProtected: view.peerIsCopyProtected, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, generateStoreAfterDownload: strongSelf.generateStoreAfterDownload, present: { [weak self] c, a in
|
||||
|
|
@ -1470,12 +1556,13 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
return
|
||||
}
|
||||
|
||||
var entries: [MessageHistoryEntry] = []
|
||||
var messageEntries: [MessageHistoryEntry] = []
|
||||
var index = messages.count
|
||||
for message in messages.reversed() {
|
||||
entries.append(MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)))
|
||||
messageEntries.append(MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)))
|
||||
index -= 1
|
||||
}
|
||||
let entries = galleryEntriesForMessageHistoryEntries(messageEntries)
|
||||
|
||||
if entries.count > strongSelf.entries.count {
|
||||
if strongSelf.invertItemOrder {
|
||||
|
|
@ -1492,7 +1579,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
var centralItemIndex: Int?
|
||||
for entry in strongSelf.entries {
|
||||
var isCentral = false
|
||||
if entry.message.stableId == strongSelf.centralEntryStableId {
|
||||
if entry.stableId == strongSelf.centralEntryStableId {
|
||||
isCentral = true
|
||||
}
|
||||
if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, playbackRate: { return self?.playbackRate }, displayInfoOnTop: displayInfoOnTop, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, generateStoreAfterDownload: strongSelf.generateStoreAfterDownload, present: { [weak self] c, a in
|
||||
|
|
@ -1551,7 +1638,8 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
var nodeAnimatesItself = false
|
||||
|
||||
if let centralItemNode = self.galleryNode.pager.centralItemNode() {
|
||||
let message = self.entries[centralItemNode.index].message
|
||||
let entry = self.entries[centralItemNode.index]
|
||||
|
||||
self.centralItemTitle.set(centralItemNode.title())
|
||||
self.centralItemTitleView.set(centralItemNode.titleView())
|
||||
self.centralItemRightBarButtonItem.set(centralItemNode.rightBarButtonItem())
|
||||
|
|
@ -1559,17 +1647,30 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
self.centralItemNavigationStyle.set(centralItemNode.navigationStyle())
|
||||
self.centralItemFooterContentNode.set(centralItemNode.footerContent())
|
||||
self.galleryNode.pager.pagingEnabledPromise.set(centralItemNode.isPagingEnabled())
|
||||
|
||||
if let (media, _) = mediaForMessage(message: message) {
|
||||
if let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments, let transitionArguments = presentationArguments.transitionArguments(message.id, media) {
|
||||
nodeAnimatesItself = true
|
||||
if presentationArguments.animated {
|
||||
centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {})
|
||||
|
||||
let message = entry.entry.message
|
||||
let media = mediaForMessage(message: message)
|
||||
if !media.isEmpty {
|
||||
var selectedMedia: Media?
|
||||
if let mediaIndex = entry.mediaIndex {
|
||||
if mediaIndex < media.count {
|
||||
selectedMedia = media[Int(mediaIndex)].0
|
||||
}
|
||||
|
||||
self._hiddenMedia.set(.single((message.id, media)))
|
||||
} else if let media = media.first {
|
||||
selectedMedia = media.0
|
||||
}
|
||||
|
||||
if let selectedMedia {
|
||||
if let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments, let transitionArguments = presentationArguments.transitionArguments(message.id, selectedMedia) {
|
||||
nodeAnimatesItself = true
|
||||
if presentationArguments.animated {
|
||||
centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {})
|
||||
}
|
||||
|
||||
self._hiddenMedia.set(.single((message.id, selectedMedia)))
|
||||
}
|
||||
centralItemNode.activateAsInitial()
|
||||
}
|
||||
centralItemNode.activateAsInitial()
|
||||
}
|
||||
|
||||
self.onDidAppear?()
|
||||
|
|
@ -1591,7 +1692,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
|
||||
override public func didAppearInContextPreview() {
|
||||
if let centralItemNode = self.galleryNode.pager.centralItemNode() {
|
||||
let message = self.entries[centralItemNode.index].message
|
||||
let message = self.entries[centralItemNode.index].entry.message
|
||||
self.centralItemTitle.set(centralItemNode.title())
|
||||
self.centralItemTitleView.set(centralItemNode.titleView())
|
||||
self.centralItemRightBarButtonItem.set(centralItemNode.rightBarButtonItem())
|
||||
|
|
@ -1600,7 +1701,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
self.centralItemFooterContentNode.set(centralItemNode.footerContent())
|
||||
self.galleryNode.pager.pagingEnabledPromise.set(centralItemNode.isPagingEnabled())
|
||||
|
||||
if let _ = mediaForMessage(message: message) {
|
||||
if !mediaForMessage(message: message).isEmpty {
|
||||
centralItemNode.activateAsInitial()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ class ChatImageGalleryItem: GalleryItem {
|
|||
let context: AccountContext
|
||||
let presentationData: PresentationData
|
||||
let message: Message
|
||||
let mediaIndex: Int?
|
||||
let location: MessageHistoryEntryLocation?
|
||||
let translateToLanguage: String?
|
||||
let peerIsCopyProtected: Bool
|
||||
|
|
@ -121,10 +122,11 @@ class ChatImageGalleryItem: GalleryItem {
|
|||
let openActionOptions: (GalleryControllerInteractionTapAction, Message) -> Void
|
||||
let present: (ViewController, Any?) -> Void
|
||||
|
||||
init(context: AccountContext, presentationData: PresentationData, message: Message, location: MessageHistoryEntryLocation?, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false, isSecret: Bool = false, displayInfoOnTop: Bool, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||
init(context: AccountContext, presentationData: PresentationData, message: Message, mediaIndex: Int? = nil, location: MessageHistoryEntryLocation?, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false, isSecret: Bool = false, displayInfoOnTop: Bool, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||
self.context = context
|
||||
self.presentationData = presentationData
|
||||
self.message = message
|
||||
self.mediaIndex = mediaIndex
|
||||
self.location = location
|
||||
self.translateToLanguage = translateToLanguage
|
||||
self.peerIsCopyProtected = peerIsCopyProtected
|
||||
|
|
@ -140,8 +142,11 @@ class ChatImageGalleryItem: GalleryItem {
|
|||
|
||||
node.setMessage(self.message, displayInfo: !self.displayInfoOnTop, translateToLanguage: self.translateToLanguage, peerIsCopyProtected: self.peerIsCopyProtected, isSecret: self.isSecret)
|
||||
for media in self.message.media {
|
||||
if let paidContent = media as? TelegramMediaPaidContent, let extendedMedia = paidContent.extendedMedia.first, case let .full(fullMedia) = extendedMedia, let image = fullMedia as? TelegramMediaImage {
|
||||
node.setImage(userLocation: .peer(self.message.id.peerId), imageReference: .message(message: MessageReference(self.message), media: image))
|
||||
if let paidContent = media as? TelegramMediaPaidContent {
|
||||
let mediaIndex = self.mediaIndex ?? 0
|
||||
if case let .full(fullMedia) = paidContent.extendedMedia[Int(mediaIndex)], let image = fullMedia as? TelegramMediaImage {
|
||||
node.setImage(userLocation: .peer(self.message.id.peerId), imageReference: .message(message: MessageReference(self.message), media: image))
|
||||
}
|
||||
} else if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia, let image = fullMedia as? TelegramMediaImage {
|
||||
node.setImage(userLocation: .peer(self.message.id.peerId), imageReference: .message(message: MessageReference(self.message), media: image))
|
||||
} else if let image = media as? TelegramMediaImage {
|
||||
|
|
@ -528,7 +533,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
})
|
||||
})))
|
||||
|
||||
if !message.isCopyProtected() && !self.peerIsCopyProtected, let media = self.contextAndMedia?.1 {
|
||||
if !message.isCopyProtected() && !self.peerIsCopyProtected && message.paidContent == nil, let media = self.contextAndMedia?.1 {
|
||||
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Gallery_SaveImage, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
|
||||
|
|
|
|||
|
|
@ -2573,7 +2573,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
}
|
||||
}
|
||||
|
||||
if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected {
|
||||
if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveVideo, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in
|
||||
f(.default)
|
||||
|
||||
|
|
|
|||
|
|
@ -525,7 +525,8 @@ public final class SecretMediaPreviewController: ViewController {
|
|||
}
|
||||
}
|
||||
|
||||
guard let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)), streamVideos: false, hideControls: true, isSecret: true, playbackRate: { nil }, peerIsCopyProtected: true, tempFilePath: tempFilePath, playbackCompleted: { [weak self] in
|
||||
let entry = GalleryEntry(entry: MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)))
|
||||
guard let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: entry, streamVideos: false, hideControls: true, isSecret: true, playbackRate: { nil }, peerIsCopyProtected: true, tempFilePath: tempFilePath, playbackCompleted: { [weak self] in
|
||||
if let self {
|
||||
if self.currentNodeMessageIsViewOnce || (duration < 30.0 && !self.currentMessageIsDismissed) {
|
||||
if let node = self.controllerNode.pager.centralItemNode() as? UniversalVideoGalleryItemNode {
|
||||
|
|
|
|||
|
|
@ -131,12 +131,14 @@ final class MediaPickerGridItemNode: GridItemNode {
|
|||
private var interaction: MediaPickerInteraction?
|
||||
private var theme: PresentationTheme?
|
||||
|
||||
private struct SelectionState: Equatable {
|
||||
let selected: Bool
|
||||
let count: Int
|
||||
}
|
||||
private let selectionPromise = ValuePromise<SelectionState>(SelectionState(selected: false, count: 0))
|
||||
private let spoilerDisposable = MetaDisposable()
|
||||
var spoilerNode: SpoilerOverlayNode?
|
||||
|
||||
var priceBackgroundNode: NavigationBackgroundNode?
|
||||
var priceIconNode: ASImageNode?
|
||||
var priceLabelNode: ImmediateTextNode?
|
||||
var priceNode: PriceNode?
|
||||
|
||||
private let progressDisposable = MetaDisposable()
|
||||
|
||||
|
|
@ -252,7 +254,7 @@ final class MediaPickerGridItemNode: GridItemNode {
|
|||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
if let interaction = self.interaction, let selectionState = interaction.selectionState {
|
||||
if let interaction = self.interaction, let selectionState = interaction.selectionState {
|
||||
let selected = selectionState.isIdentifierSelected(self.identifier)
|
||||
if let selectableItem = self.selectableItem {
|
||||
let index = selectionState.index(of: selectableItem)
|
||||
|
|
@ -261,6 +263,7 @@ final class MediaPickerGridItemNode: GridItemNode {
|
|||
}
|
||||
}
|
||||
self.checkNode?.setSelected(selected, animated: animated)
|
||||
self.selectionPromise.set(SelectionState(selected: selected, count: selectionState.selectedItems().count))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -288,6 +291,7 @@ final class MediaPickerGridItemNode: GridItemNode {
|
|||
self.typeIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.durationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.draftNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.priceNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
if animateSpoilerNode {
|
||||
self.spoilerNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
|
|
@ -564,12 +568,12 @@ final class MediaPickerGridItemNode: GridItemNode {
|
|||
}
|
||||
}
|
||||
|
||||
self.spoilerDisposable.set((combineLatest(spoilerSignal, priceSignal)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] hasSpoiler, price in
|
||||
self.spoilerDisposable.set((combineLatest(spoilerSignal, priceSignal, self.selectionPromise.get())
|
||||
|> deliverOnMainQueue).start(next: { [weak self] hasSpoiler, price, selectionState in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.updateHasSpoiler(hasSpoiler, price: price)
|
||||
strongSelf.updateHasSpoiler(hasSpoiler, price: selectionState.selected ? price : nil, isSingle: selectionState.count == 1)
|
||||
}))
|
||||
|
||||
if self.currentDraftState != nil {
|
||||
|
|
@ -635,7 +639,7 @@ final class MediaPickerGridItemNode: GridItemNode {
|
|||
}
|
||||
|
||||
private var didSetupSpoiler = false
|
||||
private func updateHasSpoiler(_ hasSpoiler: Bool, price: Int64?) {
|
||||
private func updateHasSpoiler(_ hasSpoiler: Bool, price: Int64?, isSingle: Bool) {
|
||||
var animated = true
|
||||
if !self.didSetupSpoiler {
|
||||
animated = false
|
||||
|
|
@ -659,49 +663,35 @@ final class MediaPickerGridItemNode: GridItemNode {
|
|||
self.spoilerNode?.frame = CGRect(origin: .zero, size: bounds.size)
|
||||
|
||||
if let price {
|
||||
let backgroundNode: NavigationBackgroundNode
|
||||
let labelNode: ImmediateTextNode
|
||||
let iconNode: ASImageNode
|
||||
|
||||
if let currentBackground = self.priceBackgroundNode, let currentLabel = self.priceLabelNode, let currentIcon = self.priceIconNode {
|
||||
backgroundNode = currentBackground
|
||||
labelNode = currentLabel
|
||||
iconNode = currentIcon
|
||||
let priceNode: PriceNode
|
||||
if let currentPriceNode = self.priceNode {
|
||||
priceNode = currentPriceNode
|
||||
} else {
|
||||
backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x000000, alpha: 0.5), enableBlur: true)
|
||||
labelNode = ImmediateTextNode()
|
||||
iconNode = ASImageNode()
|
||||
iconNode.displaysAsynchronously = false
|
||||
iconNode.image = UIImage(bundleImageName: "Premium/Stars/StarSmall")
|
||||
|
||||
priceNode = PriceNode()
|
||||
if let spoilerNode = self.spoilerNode {
|
||||
self.insertSubnode(backgroundNode, aboveSubnode: spoilerNode)
|
||||
self.insertSubnode(priceNode, aboveSubnode: spoilerNode)
|
||||
}
|
||||
backgroundNode.addSubnode(labelNode)
|
||||
backgroundNode.addSubnode(iconNode)
|
||||
self.priceNode = priceNode
|
||||
|
||||
self.priceBackgroundNode = backgroundNode
|
||||
self.priceLabelNode = labelNode
|
||||
self.priceIconNode = iconNode
|
||||
if animated {
|
||||
priceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
labelNode.attributedText = NSAttributedString(string: "\(price)", font: Font.semibold(15.0), textColor: .white)
|
||||
|
||||
let labelSize = labelNode.updateLayout(CGSize(width: 200.0, height: 50.0))
|
||||
let size = CGSize(width: labelSize.width + 40.0, height: 34.0)
|
||||
|
||||
backgroundNode.update(size: size, cornerRadius: 17.0, transition: .immediate)
|
||||
backgroundNode.frame = CGRect(origin: CGPoint(x: floor((bounds.width - size.width) / 2.0), y: floor((bounds.height - size.height) / 2.0)), size: size)
|
||||
|
||||
if let icon = iconNode.image {
|
||||
iconNode.frame = CGRect(origin: CGPoint(x: 10.0, y: floor((size.height - icon.size.height) / 2.0)), size: icon.size)
|
||||
}
|
||||
labelNode.frame = CGRect(origin: CGPoint(x: 30.0, y: floor((size.height - labelSize.height) / 2.0)), size: labelSize)
|
||||
self.priceNode?.update(size: bounds.size, price: isSingle ? price : nil, small: true, transition: .immediate)
|
||||
}
|
||||
} else if let spoilerNode = self.spoilerNode {
|
||||
self.spoilerNode = nil
|
||||
spoilerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak spoilerNode] _ in
|
||||
spoilerNode?.removeFromSupernode()
|
||||
})
|
||||
|
||||
if let priceNode = self.priceNode {
|
||||
self.priceNode = nil
|
||||
priceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak priceNode] _ in
|
||||
priceNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2496,30 +2496,6 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
self?.groupedValue = false
|
||||
self?.controllerNode.send(asFile: false, silently: false, scheduleTime: nil, animated: true, parameters: nil, completion: {})
|
||||
})))
|
||||
|
||||
// if !items.isEmpty {
|
||||
// items.append(.separator)
|
||||
// }
|
||||
// items.append(.action(ContextMenuActionItem(text: strings.Attachment_Grouped, icon: { theme in
|
||||
// if !grouped {
|
||||
// return nil
|
||||
// }
|
||||
// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
|
||||
// }, action: { [weak self] _, f in
|
||||
// f(.default)
|
||||
//
|
||||
// self?.groupedValue = true
|
||||
// })))
|
||||
// items.append(.action(ContextMenuActionItem(text: strings.Attachment_Ungrouped, icon: { theme in
|
||||
// if grouped {
|
||||
// return nil
|
||||
// }
|
||||
// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
|
||||
// }, action: { [weak self] _, f in
|
||||
// f(.default)
|
||||
//
|
||||
// self?.groupedValue = false
|
||||
// })))
|
||||
}
|
||||
|
||||
var isPaidAvailable = false
|
||||
|
|
@ -2585,7 +2561,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
return
|
||||
}
|
||||
|
||||
let controller = self.context.sharedContext.makeStarsAmountScreen(context: self.context, completion: { [weak self] amount in
|
||||
let controller = self.context.sharedContext.makeStarsAmountScreen(context: self.context, initialValue: price, completion: { [weak self] amount in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -478,6 +478,81 @@ private class MediaPickerSelectedItemNode: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
|
||||
final class PriceNode: ASDisplayNode {
|
||||
let backgroundNode: NavigationBackgroundNode
|
||||
let iconNode: ASImageNode
|
||||
let lockNode: ASImageNode
|
||||
let labelNode: ImmediateTextNode
|
||||
|
||||
override init() {
|
||||
self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x000000, alpha: 0.35), enableBlur: true)
|
||||
|
||||
self.lockNode = ASImageNode()
|
||||
self.lockNode.displaysAsynchronously = false
|
||||
self.lockNode.image = generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Lock"), color: .white)
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.image = UIImage(bundleImageName: "Premium/Stars/StarSmall")
|
||||
|
||||
self.labelNode = ImmediateTextNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.backgroundNode.addSubnode(self.lockNode)
|
||||
self.backgroundNode.addSubnode(self.iconNode)
|
||||
self.backgroundNode.addSubnode(self.labelNode)
|
||||
}
|
||||
|
||||
func update(size: CGSize, price: Int64?, small: Bool, transition: ContainedViewLayoutTransition) {
|
||||
var nodeSize = CGSize(width: 50.0, height: 34.0)
|
||||
var labelSize: CGSize = .zero
|
||||
|
||||
var backgroundTransition = transition
|
||||
let labelTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||
if let price {
|
||||
//TODO:localize
|
||||
self.labelNode.attributedText = NSAttributedString(string: "\(price)", font: Font.semibold(15.0), textColor: .white)
|
||||
|
||||
labelSize = self.labelNode.updateLayout(CGSize(width: 240.0, height: 50.0))
|
||||
nodeSize.width = labelSize.width + 40.0
|
||||
|
||||
if self.labelNode.alpha != 1.0 && self.backgroundNode.frame.width > 0.0 {
|
||||
backgroundTransition = labelTransition
|
||||
}
|
||||
|
||||
labelTransition.updateAlpha(node: self.labelNode, alpha: 1.0)
|
||||
labelTransition.updateAlpha(node: self.lockNode, alpha: 0.0)
|
||||
} else {
|
||||
if self.labelNode.alpha != 0.0 && self.backgroundNode.frame.width > 0.0 {
|
||||
backgroundTransition = labelTransition
|
||||
}
|
||||
|
||||
labelTransition.updateAlpha(node: self.labelNode, alpha: 0.0)
|
||||
labelTransition.updateAlpha(node: self.lockNode, alpha: 1.0)
|
||||
}
|
||||
|
||||
|
||||
self.backgroundNode.update(size: nodeSize, cornerRadius: 17.0, transition: backgroundTransition)
|
||||
backgroundTransition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: floor((size.width - nodeSize.width) / 2.0), y: floor((size.height - nodeSize.height) / 2.0)), size: nodeSize))
|
||||
|
||||
if let _ = price {
|
||||
if let icon = self.iconNode.image {
|
||||
self.iconNode.frame = CGRect(origin: CGPoint(x: 9.0 - UIScreenPixel, y: floor((nodeSize.height - icon.size.height) / 2.0)), size: icon.size)
|
||||
}
|
||||
self.labelNode.frame = CGRect(origin: CGPoint(x: 30.0, y: floor((nodeSize.height - labelSize.height) / 2.0)), size: labelSize)
|
||||
} else {
|
||||
if let icon = self.iconNode.image {
|
||||
self.iconNode.frame = CGRect(origin: CGPoint(x: 9.0 - UIScreenPixel, y: floor((nodeSize.height - icon.size.height) / 2.0)), size: icon.size)
|
||||
}
|
||||
if let icon = self.lockNode.image {
|
||||
self.lockNode.frame = CGRect(origin: CGPoint(x: 28.0, y: floor((nodeSize.height - icon.size.height) / 2.0)), size: icon.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MessageBackgroundNode: ASDisplayNode {
|
||||
private let backgroundWallpaperNode: ChatMessageBubbleBackdrop
|
||||
private let backgroundNode: ChatMessageBackground
|
||||
|
|
@ -535,6 +610,7 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS
|
|||
private let scrollNode: ASScrollNode
|
||||
private var backgroundNodes: [Int: MessageBackgroundNode] = [:]
|
||||
private var itemNodes: [String: MediaPickerSelectedItemNode] = [:]
|
||||
private var priceNodes: [Int: PriceNode] = [:]
|
||||
|
||||
private var reorderFeedback: HapticFeedback?
|
||||
private var reorderNode: ReorderingItemNode?
|
||||
|
|
@ -649,6 +725,14 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS
|
|||
}
|
||||
}
|
||||
|
||||
for (_, priceNode) in strongSelf.priceNodes {
|
||||
priceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.1)
|
||||
if strongSelf.isExternalPreview {
|
||||
ComponentTransition.immediate.setScale(layer: priceNode.layer, scale: 0.001)
|
||||
transition.updateTransformScale(layer: priceNode.layer, scale: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
for (identifier, itemNode) in strongSelf.itemNodes {
|
||||
if !strongSelf.isObscuredExternalPreview, let (transitionView, _, _) = strongSelf.getTransitionView(identifier) {
|
||||
itemNode.animateFrom(transitionView, transition: transition)
|
||||
|
|
@ -690,6 +774,10 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS
|
|||
itemNode.layer.removeAllAnimations()
|
||||
}
|
||||
|
||||
for (_, priceNode) in strongSelf.priceNodes {
|
||||
priceNode.layer.removeAllAnimations()
|
||||
}
|
||||
|
||||
strongSelf.messageNodes?.first?.layer.removeAllAnimations()
|
||||
strongSelf.messageNodes?.last?.layer.removeAllAnimations()
|
||||
|
||||
|
|
@ -710,6 +798,13 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS
|
|||
}
|
||||
}
|
||||
|
||||
for (_, priceNode) in self.priceNodes {
|
||||
priceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false)
|
||||
if self.isExternalPreview {
|
||||
transition.updateTransformScale(layer: priceNode.layer, scale: 0.001)
|
||||
}
|
||||
}
|
||||
|
||||
for (identifier, itemNode) in self.itemNodes {
|
||||
if !self.isObscuredExternalPreview, let (transitionView, maybeDustNode, completion) = self.getTransitionView(identifier) {
|
||||
itemNode.animateTo(transitionView, dustNode: maybeDustNode, transition: transition, completion: completion)
|
||||
|
|
@ -844,6 +939,8 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS
|
|||
let sideInset: CGFloat = 34.0
|
||||
let boundingWidth = min(320.0, size.width - insets.left - insets.right - sideInset * 2.0)
|
||||
|
||||
var price: Int64?
|
||||
|
||||
var validIds: [String] = []
|
||||
for item in items {
|
||||
guard let asset = item as? TGMediaEditableItem, let identifier = asset.uniqueIdentifier else {
|
||||
|
|
@ -872,6 +969,10 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS
|
|||
} else {
|
||||
itemSizes.append(asset.originalSize ?? CGSize())
|
||||
}
|
||||
|
||||
if price == nil, let priceValue = self.interaction?.editingState.price(for: asset) as? Int64 {
|
||||
price = priceValue
|
||||
}
|
||||
}
|
||||
|
||||
if !self.didSetReady {
|
||||
|
|
@ -1062,6 +1163,31 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS
|
|||
}
|
||||
}
|
||||
|
||||
if let price {
|
||||
let priceNode: PriceNode
|
||||
if let current = self.priceNodes[groupIndex] {
|
||||
priceNode = current
|
||||
} else {
|
||||
priceNode = PriceNode()
|
||||
self.priceNodes[groupIndex] = priceNode
|
||||
self.scrollNode.addSubnode(priceNode)
|
||||
}
|
||||
|
||||
if priceNode.frame.width.isZero {
|
||||
itemTransition = .immediate
|
||||
}
|
||||
|
||||
let priceNodeFrame = groupRect
|
||||
itemTransition.updatePosition(node: priceNode, position: priceNodeFrame.center)
|
||||
itemTransition.updateBounds(node: priceNode, bounds: CGRect(origin: CGPoint(), size: priceNodeFrame.size))
|
||||
priceNode.update(size: priceNode.frame.size, price: price, small: false, transition: itemTransition)
|
||||
} else if let priceNode = self.priceNodes[groupIndex] {
|
||||
self.priceNodes[groupIndex] = nil
|
||||
priceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak priceNode] _ in
|
||||
priceNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
|
||||
contentHeight += groupSize.height
|
||||
contentWidth = max(contentWidth, groupSize.width)
|
||||
groupIndex += 1
|
||||
|
|
@ -1069,7 +1195,7 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS
|
|||
|
||||
if let dragNode = self.messageNodes?.last {
|
||||
transition.updateAlpha(node: dragNode, alpha: items.count > 1 ? 1.0 : 0.0)
|
||||
transition.updateFrame(node: dragNode, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top + contentHeight + 1.0), size: dragNode.frame.size))
|
||||
transition.updateFrame(node: dragNode, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top + contentHeight + 9.0), size: dragNode.frame.size))
|
||||
|
||||
var dragNodeFrame = dragNode.frame
|
||||
dragNodeFrame.origin.y = size.height - dragNodeFrame.origin.y - dragNodeFrame.size.height
|
||||
|
|
|
|||
|
|
@ -1077,6 +1077,26 @@ private final class DemoSheetContent: CombinedComponent {
|
|||
)
|
||||
)
|
||||
)
|
||||
//TODO:localize
|
||||
availableItems[.messageEffects] = DemoPagerComponent.Item(
|
||||
AnyComponentWithIdentity(
|
||||
id: PremiumDemoScreen.Subject.messageEffects,
|
||||
component: AnyComponent(
|
||||
PageComponent(
|
||||
content: AnyComponent(PhoneDemoComponent(
|
||||
context: component.context,
|
||||
position: .top,
|
||||
model: .island,
|
||||
videoFile: configuration.videos["effects"],
|
||||
decoration: .swirlStars
|
||||
)),
|
||||
title: strings.Premium_MessageEffects,
|
||||
text: strings.Premium_MessageEffectsInfo,
|
||||
textColor: textColor
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
let index: Int = 0
|
||||
var items: [DemoPagerComponent.Item] = []
|
||||
|
|
@ -1172,6 +1192,9 @@ private final class DemoSheetContent: CombinedComponent {
|
|||
text = strings.Premium_MessagePrivacyInfo
|
||||
case .folderTags:
|
||||
text = strings.Premium_FolderTagsStandaloneInfo
|
||||
case .messageEffects:
|
||||
//TODO:localize
|
||||
text = "Add over 500 animated effects to private messages."
|
||||
default:
|
||||
text = ""
|
||||
}
|
||||
|
|
@ -1441,6 +1464,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
|
|||
case messagePrivacy
|
||||
case business
|
||||
case folderTags
|
||||
case messageEffects
|
||||
|
||||
case businessLocation
|
||||
case businessHours
|
||||
|
|
@ -1497,6 +1521,8 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
|
|||
return .business
|
||||
case .folderTags:
|
||||
return .folderTags
|
||||
case .messageEffects:
|
||||
return .messageEffects
|
||||
case .businessLocation:
|
||||
return .businessLocation
|
||||
case .businessHours:
|
||||
|
|
|
|||
|
|
@ -426,18 +426,19 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
|||
UIColor(rgb: 0xef6922),
|
||||
UIColor(rgb: 0xe95a2c),
|
||||
UIColor(rgb: 0xe74e33),
|
||||
UIColor(rgb: 0xe74e33), //replace
|
||||
UIColor(rgb: 0xe74e33),
|
||||
UIColor(rgb: 0xe54937),
|
||||
UIColor(rgb: 0xe3433c),
|
||||
UIColor(rgb: 0xdb374b),
|
||||
UIColor(rgb: 0xcb3e6d),
|
||||
UIColor(rgb: 0xbc4395),
|
||||
UIColor(rgb: 0xab4ac4),
|
||||
UIColor(rgb: 0xab4ac4),
|
||||
UIColor(rgb: 0xa34cd7),
|
||||
UIColor(rgb: 0x9b4fed),
|
||||
UIColor(rgb: 0x8958ff),
|
||||
UIColor(rgb: 0x676bff),
|
||||
UIColor(rgb: 0x676bff), //replace
|
||||
UIColor(rgb: 0x676bff),
|
||||
UIColor(rgb: 0x6172ff),
|
||||
UIColor(rgb: 0x5b79ff),
|
||||
UIColor(rgb: 0x4492ff),
|
||||
|
|
@ -534,6 +535,8 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
|||
demoSubject = .lastSeen
|
||||
case .messagePrivacy:
|
||||
demoSubject = .messagePrivacy
|
||||
case .messageEffects:
|
||||
demoSubject = .messageEffects
|
||||
case .business:
|
||||
demoSubject = .business
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -296,6 +296,12 @@ public enum PremiumSource: Equatable {
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
case .messageEffects:
|
||||
if case .messageEffects = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -342,6 +348,7 @@ public enum PremiumSource: Equatable {
|
|||
case readTime
|
||||
case messageTags
|
||||
case folderTags
|
||||
case messageEffects
|
||||
|
||||
var identifier: String? {
|
||||
switch self {
|
||||
|
|
@ -433,6 +440,8 @@ public enum PremiumSource: Equatable {
|
|||
return "saved_tags"
|
||||
case .folderTags:
|
||||
return "folder_tags"
|
||||
case .messageEffects:
|
||||
return "effects"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -460,6 +469,7 @@ public enum PremiumPerk: CaseIterable {
|
|||
case messagePrivacy
|
||||
case business
|
||||
case folderTags
|
||||
case messageEffects
|
||||
|
||||
case businessLocation
|
||||
case businessHours
|
||||
|
|
@ -493,7 +503,8 @@ public enum PremiumPerk: CaseIterable {
|
|||
.lastSeen,
|
||||
.messagePrivacy,
|
||||
.folderTags,
|
||||
.business
|
||||
.business,
|
||||
.messageEffects
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -565,6 +576,8 @@ public enum PremiumPerk: CaseIterable {
|
|||
return "message_privacy"
|
||||
case .folderTags:
|
||||
return "folder_tags"
|
||||
case .messageEffects:
|
||||
return "effects"
|
||||
case .business:
|
||||
return "business"
|
||||
case .businessLocation:
|
||||
|
|
@ -632,7 +645,8 @@ public enum PremiumPerk: CaseIterable {
|
|||
return strings.Premium_FolderTags
|
||||
case .business:
|
||||
return strings.Premium_Business
|
||||
|
||||
case .messageEffects:
|
||||
return strings.Premium_MessageEffects
|
||||
case .businessLocation:
|
||||
return strings.Business_Location
|
||||
case .businessHours:
|
||||
|
|
@ -698,7 +712,8 @@ public enum PremiumPerk: CaseIterable {
|
|||
return strings.Premium_FolderTagsInfo
|
||||
case .business:
|
||||
return strings.Premium_BusinessInfo
|
||||
|
||||
case .messageEffects:
|
||||
return strings.Premium_MessageEffectsInfo
|
||||
case .businessLocation:
|
||||
return strings.Business_LocationInfo
|
||||
case .businessHours:
|
||||
|
|
@ -764,6 +779,8 @@ public enum PremiumPerk: CaseIterable {
|
|||
return "Premium/Perk/MessageTags"
|
||||
case .business:
|
||||
return "Premium/Perk/Business"
|
||||
case .messageEffects:
|
||||
return "Premium/Perk/MessageEffects"
|
||||
|
||||
case .businessLocation:
|
||||
return "Premium/BusinessPerk/Location"
|
||||
|
|
@ -797,6 +814,7 @@ struct PremiumIntroConfiguration {
|
|||
.translation,
|
||||
.animatedEmoji,
|
||||
.emojiStatus,
|
||||
.messageEffects,
|
||||
.messageTags,
|
||||
.colors,
|
||||
.wallpapers,
|
||||
|
|
@ -1835,18 +1853,19 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||
UIColor(rgb: 0xef6922),
|
||||
UIColor(rgb: 0xe95a2c),
|
||||
UIColor(rgb: 0xe74e33),
|
||||
UIColor(rgb: 0xe74e33), //replace
|
||||
UIColor(rgb: 0xe74e33),
|
||||
UIColor(rgb: 0xe54937),
|
||||
UIColor(rgb: 0xe3433c),
|
||||
UIColor(rgb: 0xdb374b),
|
||||
UIColor(rgb: 0xcb3e6d),
|
||||
UIColor(rgb: 0xbc4395),
|
||||
UIColor(rgb: 0xab4ac4),
|
||||
UIColor(rgb: 0xab4ac4),
|
||||
UIColor(rgb: 0xa34cd7),
|
||||
UIColor(rgb: 0x9b4fed),
|
||||
UIColor(rgb: 0x8958ff),
|
||||
UIColor(rgb: 0x676bff),
|
||||
UIColor(rgb: 0x676bff), //replace
|
||||
UIColor(rgb: 0x676bff),
|
||||
UIColor(rgb: 0x6172ff),
|
||||
UIColor(rgb: 0x5b79ff),
|
||||
UIColor(rgb: 0x4492ff),
|
||||
|
|
@ -2071,6 +2090,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||
demoSubject = .lastSeen
|
||||
case .messagePrivacy:
|
||||
demoSubject = .messagePrivacy
|
||||
case .messageEffects:
|
||||
demoSubject = .messageEffects
|
||||
case .business:
|
||||
demoSubject = .business
|
||||
let _ = ApplicationSpecificNotice.setDismissedBusinessBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
|
||||
|
|
|
|||
|
|
@ -826,6 +826,26 @@ public class PremiumLimitsListScreen: ViewController {
|
|||
)
|
||||
)
|
||||
)
|
||||
//TODO:localize
|
||||
availableItems[.messageEffects] = DemoPagerComponent.Item(
|
||||
AnyComponentWithIdentity(
|
||||
id: PremiumDemoScreen.Subject.messageEffects,
|
||||
component: AnyComponent(
|
||||
PageComponent(
|
||||
content: AnyComponent(PhoneDemoComponent(
|
||||
context: context,
|
||||
position: .top,
|
||||
model: .island,
|
||||
videoFile: videos["effects"],
|
||||
decoration: .swirlStars
|
||||
)),
|
||||
title: strings.Premium_MessageEffects,
|
||||
text: strings.Premium_MessageEffectsInfo,
|
||||
textColor: textColor
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
availableItems[.business] = DemoPagerComponent.Item(
|
||||
AnyComponentWithIdentity(
|
||||
id: PremiumDemoScreen.Subject.business,
|
||||
|
|
|
|||
|
|
@ -50,6 +50,10 @@ swift_library(
|
|||
"//submodules/PasswordSetupUI",
|
||||
"//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController",
|
||||
"//submodules/TelegramUI/Components/ListItemComponentAdaptor",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsAvatarComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import StoryContainerScreen
|
|||
import TelegramNotices
|
||||
import ComponentFlow
|
||||
import BoostLevelIconComponent
|
||||
import StarsWithdrawalScreen
|
||||
|
||||
private let initialBoostersDisplayedLimit: Int32 = 5
|
||||
private let initialTransactionsDisplayedLimit: Int32 = 5
|
||||
|
|
@ -42,17 +43,20 @@ private final class ChannelStatsControllerArguments {
|
|||
let openGifts: () -> Void
|
||||
let createPrepaidGiveaway: (PrepaidGiveaway) -> Void
|
||||
let updateGiftsSelected: (Bool) -> Void
|
||||
let updateStarsSelected: (Bool) -> Void
|
||||
|
||||
let requestWithdraw: () -> Void
|
||||
let requestTonWithdraw: () -> Void
|
||||
let requestStarsWithdraw: () -> Void
|
||||
let openMonetizationIntro: () -> Void
|
||||
let openMonetizationInfo: () -> Void
|
||||
let openTransaction: (RevenueStatsTransactionsContext.State.Transaction) -> Void
|
||||
let openTonTransaction: (RevenueStatsTransactionsContext.State.Transaction) -> Void
|
||||
let openStarsTransaction: (StarsContext.State.Transaction) -> Void
|
||||
let expandTransactions: () -> Void
|
||||
let updateCpmEnabled: (Bool) -> Void
|
||||
let presentCpmLocked: () -> Void
|
||||
let dismissInput: () -> Void
|
||||
|
||||
init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openPostStats: @escaping (EnginePeer, StatsPostItem) -> Void, openStory: @escaping (EngineStoryItem, UIView) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openBoost: @escaping (ChannelBoostersContext.State.Boost) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void, requestWithdraw: @escaping () -> Void, openMonetizationIntro: @escaping () -> Void, openMonetizationInfo: @escaping () -> Void, openTransaction: @escaping (RevenueStatsTransactionsContext.State.Transaction) -> Void, expandTransactions: @escaping () -> Void, updateCpmEnabled: @escaping (Bool) -> Void, presentCpmLocked: @escaping () -> Void, dismissInput: @escaping () -> Void) {
|
||||
init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openPostStats: @escaping (EnginePeer, StatsPostItem) -> Void, openStory: @escaping (EngineStoryItem, UIView) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openBoost: @escaping (ChannelBoostersContext.State.Boost) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void, updateStarsSelected: @escaping (Bool) -> Void, requestTonWithdraw: @escaping () -> Void, requestStarsWithdraw: @escaping () -> Void, openMonetizationIntro: @escaping () -> Void, openMonetizationInfo: @escaping () -> Void, openTonTransaction: @escaping (RevenueStatsTransactionsContext.State.Transaction) -> Void, openStarsTransaction: @escaping (StarsContext.State.Transaction) -> Void, expandTransactions: @escaping () -> Void, updateCpmEnabled: @escaping (Bool) -> Void, presentCpmLocked: @escaping () -> Void, dismissInput: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.loadDetailedGraph = loadDetailedGraph
|
||||
self.openPostStats = openPostStats
|
||||
|
|
@ -65,10 +69,13 @@ private final class ChannelStatsControllerArguments {
|
|||
self.openGifts = openGifts
|
||||
self.createPrepaidGiveaway = createPrepaidGiveaway
|
||||
self.updateGiftsSelected = updateGiftsSelected
|
||||
self.requestWithdraw = requestWithdraw
|
||||
self.updateStarsSelected = updateStarsSelected
|
||||
self.requestTonWithdraw = requestTonWithdraw
|
||||
self.requestStarsWithdraw = requestStarsWithdraw
|
||||
self.openMonetizationIntro = openMonetizationIntro
|
||||
self.openMonetizationInfo = openMonetizationInfo
|
||||
self.openTransaction = openTransaction
|
||||
self.openTonTransaction = openTonTransaction
|
||||
self.openStarsTransaction = openStarsTransaction
|
||||
self.expandTransactions = expandTransactions
|
||||
self.updateCpmEnabled = updateCpmEnabled
|
||||
self.presentCpmLocked = presentCpmLocked
|
||||
|
|
@ -101,9 +108,11 @@ private enum StatsSection: Int32 {
|
|||
|
||||
case adsHeader
|
||||
case adsImpressions
|
||||
case adsRevenue
|
||||
case adsTonRevenue
|
||||
case adsStarsRevenue
|
||||
case adsProceeds
|
||||
case adsBalance
|
||||
case adsTonBalance
|
||||
case adsStarsBalance
|
||||
case adsTransactions
|
||||
case adsCpm
|
||||
}
|
||||
|
|
@ -218,18 +227,27 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||
case adsImpressionsTitle(PresentationTheme, String)
|
||||
case adsImpressionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType)
|
||||
|
||||
case adsRevenueTitle(PresentationTheme, String)
|
||||
case adsRevenueGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType, Double)
|
||||
case adsTonRevenueTitle(PresentationTheme, String)
|
||||
case adsTonRevenueGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType, Double)
|
||||
|
||||
case adsStarsRevenueTitle(PresentationTheme, String)
|
||||
case adsStarsRevenueGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType, Double)
|
||||
|
||||
case adsProceedsTitle(PresentationTheme, String)
|
||||
case adsProceedsOverview(PresentationTheme, RevenueStats, TelegramMediaFile?)
|
||||
case adsProceedsOverview(PresentationTheme, RevenueStats, StarsRevenueStats?)
|
||||
|
||||
case adsBalanceTitle(PresentationTheme, String)
|
||||
case adsBalance(PresentationTheme, RevenueStats, Bool, Bool, TelegramMediaFile?)
|
||||
case adsBalanceInfo(PresentationTheme, String)
|
||||
case adsTonBalanceTitle(PresentationTheme, String)
|
||||
case adsTonBalance(PresentationTheme, RevenueStats, Bool, Bool)
|
||||
case adsTonBalanceInfo(PresentationTheme, String)
|
||||
|
||||
case adsStarsBalanceTitle(PresentationTheme, String)
|
||||
case adsStarsBalance(PresentationTheme, StarsRevenueStats, Bool, Bool)
|
||||
case adsStarsBalanceInfo(PresentationTheme, String)
|
||||
|
||||
case adsTransactionsTitle(PresentationTheme, String)
|
||||
case adsTransactionsTabs(PresentationTheme, String, String, Bool)
|
||||
case adsTransaction(Int32, PresentationTheme, RevenueStatsTransactionsContext.State.Transaction)
|
||||
case adsStarsTransaction(Int32, PresentationTheme, StarsContext.State.Transaction)
|
||||
case adsTransactionsExpand(PresentationTheme, String)
|
||||
|
||||
case adsCpmToggle(PresentationTheme, String, Int32, Bool?)
|
||||
|
|
@ -281,13 +299,17 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||
return StatsSection.adsHeader.rawValue
|
||||
case .adsImpressionsTitle, .adsImpressionsGraph:
|
||||
return StatsSection.adsImpressions.rawValue
|
||||
case .adsRevenueTitle, .adsRevenueGraph:
|
||||
return StatsSection.adsRevenue.rawValue
|
||||
case .adsTonRevenueTitle, .adsTonRevenueGraph:
|
||||
return StatsSection.adsTonRevenue.rawValue
|
||||
case .adsStarsRevenueTitle, .adsStarsRevenueGraph:
|
||||
return StatsSection.adsStarsRevenue.rawValue
|
||||
case .adsProceedsTitle, .adsProceedsOverview:
|
||||
return StatsSection.adsProceeds.rawValue
|
||||
case .adsBalanceTitle, .adsBalance, .adsBalanceInfo:
|
||||
return StatsSection.adsBalance.rawValue
|
||||
case .adsTransactionsTitle, .adsTransaction, .adsTransactionsExpand:
|
||||
case .adsTonBalanceTitle, .adsTonBalance, .adsTonBalanceInfo:
|
||||
return StatsSection.adsTonBalance.rawValue
|
||||
case .adsStarsBalanceTitle, .adsStarsBalance, .adsStarsBalanceInfo:
|
||||
return StatsSection.adsStarsBalance.rawValue
|
||||
case .adsTransactionsTitle, .adsTransactionsTabs, .adsTransaction, .adsStarsTransaction, .adsTransactionsExpand:
|
||||
return StatsSection.adsTransactions.rawValue
|
||||
case .adsCpmToggle, .adsCpmInfo:
|
||||
return StatsSection.adsCpm.rawValue
|
||||
|
|
@ -392,30 +414,44 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||
return 20001
|
||||
case .adsImpressionsGraph:
|
||||
return 20002
|
||||
case .adsRevenueTitle:
|
||||
case .adsTonRevenueTitle:
|
||||
return 20003
|
||||
case .adsRevenueGraph:
|
||||
case .adsTonRevenueGraph:
|
||||
return 20004
|
||||
case .adsProceedsTitle:
|
||||
case .adsStarsRevenueTitle:
|
||||
return 20005
|
||||
case .adsProceedsOverview:
|
||||
case .adsStarsRevenueGraph:
|
||||
return 20006
|
||||
case .adsBalanceTitle:
|
||||
case .adsProceedsTitle:
|
||||
return 20007
|
||||
case .adsBalance:
|
||||
case .adsProceedsOverview:
|
||||
return 20008
|
||||
case .adsBalanceInfo:
|
||||
case .adsTonBalanceTitle:
|
||||
return 20009
|
||||
case .adsTransactionsTitle:
|
||||
case .adsTonBalance:
|
||||
return 20010
|
||||
case .adsTonBalanceInfo:
|
||||
return 20011
|
||||
case .adsStarsBalanceTitle:
|
||||
return 20012
|
||||
case .adsStarsBalance:
|
||||
return 20013
|
||||
case .adsStarsBalanceInfo:
|
||||
return 20014
|
||||
case .adsTransactionsTitle:
|
||||
return 20015
|
||||
case .adsTransactionsTabs:
|
||||
return 20016
|
||||
case let .adsTransaction(index, _, _):
|
||||
return 20011 + index
|
||||
return 20017 + index
|
||||
case let .adsStarsTransaction(index, _, _):
|
||||
return 30017 + index
|
||||
case .adsTransactionsExpand:
|
||||
return 30000
|
||||
return 40000
|
||||
case .adsCpmToggle:
|
||||
return 30001
|
||||
return 40001
|
||||
case .adsCpmInfo:
|
||||
return 30002
|
||||
return 40002
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -709,14 +745,26 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
case let .adsRevenueTitle(lhsTheme, lhsText):
|
||||
if case let .adsRevenueTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
case let .adsTonRevenueTitle(lhsTheme, lhsText):
|
||||
if case let .adsTonRevenueTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .adsRevenueGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType, lhsRate):
|
||||
if case let .adsRevenueGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType, rhsRate) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType, lhsRate == rhsRate {
|
||||
case let .adsTonRevenueGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType, lhsRate):
|
||||
if case let .adsTonRevenueGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType, rhsRate) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType, lhsRate == rhsRate {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .adsStarsRevenueTitle(lhsTheme, lhsText):
|
||||
if case let .adsStarsRevenueTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .adsStarsRevenueGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType, lhsRate):
|
||||
if case let .adsStarsRevenueGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType, rhsRate) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType, lhsRate == rhsRate {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
|
|
@ -727,26 +775,44 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
case let .adsProceedsOverview(lhsTheme, lhsStatus, lhsAnimatedEmoji):
|
||||
if case let .adsProceedsOverview(rhsTheme, rhsStatus, rhsAnimatedEmoji) = rhs, lhsTheme === rhsTheme, lhsStatus == rhsStatus, lhsAnimatedEmoji == rhsAnimatedEmoji {
|
||||
case let .adsProceedsOverview(lhsTheme, lhsStatus, lhsStarsStatus):
|
||||
if case let .adsProceedsOverview(rhsTheme, rhsStatus, rhsStarsStatus) = rhs, lhsTheme === rhsTheme, lhsStatus == rhsStatus, lhsStarsStatus == rhsStarsStatus {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .adsBalanceTitle(lhsTheme, lhsText):
|
||||
if case let .adsBalanceTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
case let .adsTonBalanceTitle(lhsTheme, lhsText):
|
||||
if case let .adsTonBalanceTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .adsBalance(lhsTheme, lhsStats, lhsCanWithdraw, lhsIsEnabled, lhsAnimatedEmoji):
|
||||
if case let .adsBalance(rhsTheme, rhsStats, rhsCanWithdraw, rhsIsEnabled, rhsAnimatedEmoji) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsCanWithdraw == rhsCanWithdraw, lhsIsEnabled == rhsIsEnabled, lhsAnimatedEmoji == rhsAnimatedEmoji {
|
||||
case let .adsTonBalance(lhsTheme, lhsStats, lhsCanWithdraw, lhsIsEnabled):
|
||||
if case let .adsTonBalance(rhsTheme, rhsStats, rhsCanWithdraw, rhsIsEnabled) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsCanWithdraw == rhsCanWithdraw, lhsIsEnabled == rhsIsEnabled {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .adsBalanceInfo(lhsTheme, lhsText):
|
||||
if case let .adsBalanceInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
case let .adsTonBalanceInfo(lhsTheme, lhsText):
|
||||
if case let .adsTonBalanceInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .adsStarsBalanceTitle(lhsTheme, lhsText):
|
||||
if case let .adsStarsBalanceTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .adsStarsBalance(lhsTheme, lhsStats, lhsCanWithdraw, lhsIsEnabled):
|
||||
if case let .adsStarsBalance(rhsTheme, rhsStats, rhsCanWithdraw, rhsIsEnabled) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsCanWithdraw == rhsCanWithdraw, lhsIsEnabled == rhsIsEnabled {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .adsStarsBalanceInfo(lhsTheme, lhsText):
|
||||
if case let .adsStarsBalanceInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
|
|
@ -757,12 +823,24 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
case let .adsTransactionsTabs(lhsTheme, lhsTonText, lhsStarsText, lhsStarsSelected):
|
||||
if case let .adsTransactionsTabs(rhsTheme, rhsTonText, rhsStarsText, rhsStarsSelected) = rhs, lhsTheme === rhsTheme, lhsTonText == rhsTonText, lhsStarsText == rhsStarsText, lhsStarsSelected == rhsStarsSelected {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .adsTransaction(lhsIndex, lhsTheme, lhsTransaction):
|
||||
if case let .adsTransaction(rhsIndex, rhsTheme, rhsTransaction) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTransaction == rhsTransaction {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .adsStarsTransaction(lhsIndex, lhsTheme, lhsTransaction):
|
||||
if case let .adsStarsTransaction(rhsIndex, rhsTheme, rhsTransaction) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTransaction == rhsTransaction {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .adsTransactionsExpand(lhsTheme, lhsText):
|
||||
if case let .adsTransactionsExpand(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
|
|
@ -811,9 +889,11 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||
let .boostersTitle(_, text),
|
||||
let .boostLinkTitle(_, text),
|
||||
let .adsImpressionsTitle(_, text),
|
||||
let .adsRevenueTitle(_, text),
|
||||
let .adsTonRevenueTitle(_, text),
|
||||
let .adsStarsRevenueTitle(_, text),
|
||||
let .adsProceedsTitle(_, text),
|
||||
let .adsBalanceTitle(_, text),
|
||||
let .adsTonBalanceTitle(_, text),
|
||||
let .adsStarsBalanceTitle(_, text),
|
||||
let .adsTransactionsTitle(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .boostPrepaidInfo(_, text),
|
||||
|
|
@ -835,7 +915,9 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||
let .storyReactionsByEmotionGraph(_, _, _, graph, type),
|
||||
let .adsImpressionsGraph(_, _, _, graph, type):
|
||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
|
||||
case let .adsRevenueGraph(_, _, _, graph, type, rate):
|
||||
case let .adsTonRevenueGraph(_, _, _, graph, type, rate):
|
||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, conversionRate: rate, sectionId: self.section, style: .blocks)
|
||||
case let .adsStarsRevenueGraph(_, _, _, graph, type, rate):
|
||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, conversionRate: rate, sectionId: self.section, style: .blocks)
|
||||
case let .postInteractionsGraph(_, _, _, graph, type),
|
||||
let .instantPageInteractionsGraph(_, _, _, graph, type),
|
||||
|
|
@ -959,9 +1041,9 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in
|
||||
arguments.openMonetizationIntro()
|
||||
})
|
||||
case let .adsProceedsOverview(_, stats, animatedEmoji):
|
||||
return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: false, stats: stats, animatedEmoji: animatedEmoji, sectionId: self.section, style: .blocks)
|
||||
case let .adsBalance(_, stats, canWithdraw, isEnabled, _):
|
||||
case let .adsProceedsOverview(_, stats, starsStats):
|
||||
return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: false, stats: stats, additionalStats: starsStats, sectionId: self.section, style: .blocks)
|
||||
case let .adsTonBalance(_, stats, canWithdraw, isEnabled):
|
||||
return MonetizationBalanceItem(
|
||||
context: arguments.context,
|
||||
presentationData: presentationData,
|
||||
|
|
@ -969,15 +1051,36 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||
canWithdraw: canWithdraw,
|
||||
isEnabled: isEnabled,
|
||||
withdrawAction: {
|
||||
arguments.requestWithdraw()
|
||||
arguments.requestTonWithdraw()
|
||||
},
|
||||
sectionId: self.section,
|
||||
style: .blocks
|
||||
)
|
||||
case let .adsBalanceInfo(_, text):
|
||||
case let .adsTonBalanceInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in
|
||||
arguments.openMonetizationInfo()
|
||||
})
|
||||
case let .adsStarsBalance(_, stats, canWithdraw, isEnabled):
|
||||
return MonetizationBalanceItem(
|
||||
context: arguments.context,
|
||||
presentationData: presentationData,
|
||||
stats: stats,
|
||||
canWithdraw: canWithdraw,
|
||||
isEnabled: isEnabled,
|
||||
withdrawAction: {
|
||||
arguments.requestStarsWithdraw()
|
||||
},
|
||||
sectionId: self.section,
|
||||
style: .blocks
|
||||
)
|
||||
case let .adsStarsBalanceInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in
|
||||
arguments.openMonetizationInfo()
|
||||
})
|
||||
case let .adsTransactionsTabs(_, tonText, starsText, starsSelected):
|
||||
return BoostsTabsItem(theme: presentationData.theme, boostsText: tonText, giftsText: starsText, selectedTab: starsSelected ? .gifts : .boosts, sectionId: self.section, selectionUpdated: { tab in
|
||||
arguments.updateStarsSelected(tab == .gifts)
|
||||
})
|
||||
case let .adsTransaction(_, theme, transaction):
|
||||
let font = Font.with(size: floor(presentationData.fontSize.itemListBaseFontSize))
|
||||
let smallLabelFont = Font.with(size: floor(presentationData.fontSize.itemListBaseFontSize / 17.0 * 13.0))
|
||||
|
|
@ -1024,8 +1127,12 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||
}
|
||||
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: "", attributedTitle: title, label: "", attributedLabel: label, labelStyle: .coloredText(labelColor), additionalDetailLabel: detailText, additionalDetailLabelColor: detailColor, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
|
||||
arguments.openTransaction(transaction)
|
||||
arguments.openTonTransaction(transaction)
|
||||
})
|
||||
case let .adsStarsTransaction(_, _, transaction):
|
||||
return StarsTransactionItem(context: arguments.context, presentationData: presentationData, transaction: transaction, action: {
|
||||
arguments.openStarsTransaction(transaction)
|
||||
}, sectionId: self.section, style: .blocks)
|
||||
case let .adsTransactionsExpand(theme, title):
|
||||
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: {
|
||||
arguments.expandTransactions()
|
||||
|
|
@ -1062,23 +1169,26 @@ private struct ChannelStatsControllerState: Equatable {
|
|||
let boostersExpanded: Bool
|
||||
let moreBoostersDisplayed: Int32
|
||||
let giftsSelected: Bool
|
||||
let starsSelected: Bool
|
||||
let transactionsExpanded: Bool
|
||||
let moreTransactionsDisplayed: Int32
|
||||
|
||||
|
||||
init() {
|
||||
self.section = .stats
|
||||
self.boostersExpanded = false
|
||||
self.moreBoostersDisplayed = 0
|
||||
self.giftsSelected = false
|
||||
self.starsSelected = false
|
||||
self.transactionsExpanded = false
|
||||
self.moreTransactionsDisplayed = 0
|
||||
}
|
||||
|
||||
init(section: ChannelStatsSection, boostersExpanded: Bool, moreBoostersDisplayed: Int32, giftsSelected: Bool, transactionsExpanded: Bool, moreTransactionsDisplayed: Int32) {
|
||||
init(section: ChannelStatsSection, boostersExpanded: Bool, moreBoostersDisplayed: Int32, giftsSelected: Bool, starsSelected: Bool, transactionsExpanded: Bool, moreTransactionsDisplayed: Int32) {
|
||||
self.section = section
|
||||
self.boostersExpanded = boostersExpanded
|
||||
self.moreBoostersDisplayed = moreBoostersDisplayed
|
||||
self.giftsSelected = giftsSelected
|
||||
self.starsSelected = starsSelected
|
||||
self.transactionsExpanded = transactionsExpanded
|
||||
self.moreTransactionsDisplayed = moreTransactionsDisplayed
|
||||
}
|
||||
|
|
@ -1096,6 +1206,9 @@ private struct ChannelStatsControllerState: Equatable {
|
|||
if lhs.giftsSelected != rhs.giftsSelected {
|
||||
return false
|
||||
}
|
||||
if lhs.starsSelected != rhs.starsSelected {
|
||||
return false
|
||||
}
|
||||
if lhs.transactionsExpanded != rhs.transactionsExpanded {
|
||||
return false
|
||||
}
|
||||
|
|
@ -1106,27 +1219,31 @@ private struct ChannelStatsControllerState: Equatable {
|
|||
}
|
||||
|
||||
func withUpdatedSection(_ section: ChannelStatsSection) -> ChannelStatsControllerState {
|
||||
return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
|
||||
return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
|
||||
}
|
||||
|
||||
func withUpdatedBoostersExpanded(_ boostersExpanded: Bool) -> ChannelStatsControllerState {
|
||||
return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
|
||||
return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
|
||||
}
|
||||
|
||||
func withUpdatedMoreBoostersDisplayed(_ moreBoostersDisplayed: Int32) -> ChannelStatsControllerState {
|
||||
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: moreBoostersDisplayed, giftsSelected: self.giftsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
|
||||
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
|
||||
}
|
||||
|
||||
func withUpdatedGiftsSelected(_ giftsSelected: Bool) -> ChannelStatsControllerState {
|
||||
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: giftsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
|
||||
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
|
||||
}
|
||||
|
||||
func withUpdatedStarsSelected(_ giftsSelected: Bool) -> ChannelStatsControllerState {
|
||||
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
|
||||
}
|
||||
|
||||
func withUpdatedTransactionsExpanded(_ transactionsExpanded: Bool) -> ChannelStatsControllerState {
|
||||
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, transactionsExpanded: transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
|
||||
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
|
||||
}
|
||||
|
||||
func withUpdatedMoreTransactionsDisplayed(_ moreTransactionsDisplayed: Int32) -> ChannelStatsControllerState {
|
||||
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: moreTransactionsDisplayed)
|
||||
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: moreTransactionsDisplayed)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1373,13 +1490,12 @@ private func monetizationEntries(
|
|||
data: RevenueStats,
|
||||
boostData: ChannelBoostStatus?,
|
||||
transactionsInfo: RevenueStatsTransactionsContext.State,
|
||||
starsData: StarsRevenueStats?,
|
||||
starsTransactionsInfo: StarsTransactionsContext.State,
|
||||
adsRestricted: Bool,
|
||||
animatedEmojis: [String: [StickerPackItem]],
|
||||
premiumConfiguration: PremiumConfiguration,
|
||||
monetizationConfiguration: MonetizationConfiguration
|
||||
) -> [StatsEntry] {
|
||||
let diamond = animatedEmojis["💎"]?.first?.file
|
||||
|
||||
var entries: [StatsEntry] = []
|
||||
entries.append(.adsHeader(presentationData.theme, presentationData.strings.Monetization_Header))
|
||||
|
||||
|
|
@ -1389,19 +1505,24 @@ private func monetizationEntries(
|
|||
}
|
||||
|
||||
if !data.revenueGraph.isEmpty {
|
||||
entries.append(.adsRevenueTitle(presentationData.theme, presentationData.strings.Monetization_AdRevenueTitle))
|
||||
entries.append(.adsRevenueGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.revenueGraph, .currency, data.usdRate))
|
||||
entries.append(.adsTonRevenueTitle(presentationData.theme, presentationData.strings.Monetization_AdRevenueTitle))
|
||||
entries.append(.adsTonRevenueGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.revenueGraph, .currency, data.usdRate))
|
||||
}
|
||||
|
||||
if let starsData, !starsData.revenueGraph.isEmpty {
|
||||
entries.append(.adsStarsRevenueTitle(presentationData.theme, presentationData.strings.Monetization_StarsRevenueTitle))
|
||||
entries.append(.adsStarsRevenueGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, starsData.revenueGraph, .stars, starsData.usdRate))
|
||||
}
|
||||
|
||||
entries.append(.adsProceedsTitle(presentationData.theme, presentationData.strings.Monetization_OverviewTitle))
|
||||
entries.append(.adsProceedsOverview(presentationData.theme, data, diamond))
|
||||
entries.append(.adsProceedsOverview(presentationData.theme, data, starsData))
|
||||
|
||||
var isCreator = false
|
||||
if let peer, case let .channel(channel) = peer, channel.flags.contains(.isCreator) {
|
||||
isCreator = true
|
||||
}
|
||||
entries.append(.adsBalanceTitle(presentationData.theme, presentationData.strings.Monetization_BalanceTitle))
|
||||
entries.append(.adsBalance(presentationData.theme, data, isCreator && data.balances.availableBalance > 0, monetizationConfiguration.withdrawalAvailable, diamond))
|
||||
entries.append(.adsTonBalanceTitle(presentationData.theme, presentationData.strings.Monetization_TonBalanceTitle))
|
||||
entries.append(.adsTonBalance(presentationData.theme, data, isCreator && data.balances.availableBalance > 0, monetizationConfiguration.withdrawalAvailable))
|
||||
|
||||
if isCreator {
|
||||
let withdrawalInfoText: String
|
||||
|
|
@ -1412,11 +1533,35 @@ private func monetizationEntries(
|
|||
} else {
|
||||
withdrawalInfoText = presentationData.strings.Monetization_Balance_ComingLaterInfo
|
||||
}
|
||||
entries.append(.adsBalanceInfo(presentationData.theme, withdrawalInfoText))
|
||||
entries.append(.adsTonBalanceInfo(presentationData.theme, withdrawalInfoText))
|
||||
}
|
||||
|
||||
if !transactionsInfo.transactions.isEmpty {
|
||||
entries.append(.adsTransactionsTitle(presentationData.theme, presentationData.strings.Monetization_TransactionsTitle))
|
||||
|
||||
if let starsData, starsData.balances.overallRevenue > 0 {
|
||||
entries.append(.adsStarsBalanceTitle(presentationData.theme, presentationData.strings.Monetization_StarsBalanceTitle))
|
||||
entries.append(.adsStarsBalance(presentationData.theme, starsData, isCreator && starsData.balances.availableBalance > 0, starsData.balances.withdrawEnabled))
|
||||
entries.append(.adsStarsBalanceInfo(presentationData.theme, presentationData.strings.Monetization_Balance_StarsInfo))
|
||||
}
|
||||
|
||||
var addedTransactionsTabs = false
|
||||
if !transactionsInfo.transactions.isEmpty && !starsTransactionsInfo.transactions.isEmpty {
|
||||
addedTransactionsTabs = true
|
||||
entries.append(.adsTransactionsTabs(presentationData.theme, presentationData.strings.Monetization_TonTransactions, presentationData.strings.Monetization_StarsTransactions, state.starsSelected))
|
||||
}
|
||||
|
||||
var displayTonTransactions = false
|
||||
if !transactionsInfo.transactions.isEmpty && (starsTransactionsInfo.transactions.isEmpty || !state.starsSelected) {
|
||||
displayTonTransactions = true
|
||||
}
|
||||
|
||||
var displayStarsTransactions = false
|
||||
if !starsTransactionsInfo.transactions.isEmpty && (transactionsInfo.transactions.isEmpty || state.starsSelected) {
|
||||
displayStarsTransactions = true
|
||||
}
|
||||
|
||||
if displayTonTransactions {
|
||||
if !addedTransactionsTabs {
|
||||
entries.append(.adsTransactionsTitle(presentationData.theme, presentationData.strings.Monetization_TonTransactions.uppercased()))
|
||||
}
|
||||
|
||||
var transactions = transactionsInfo.transactions
|
||||
var limit: Int32
|
||||
|
|
@ -1438,7 +1583,38 @@ private func monetizationEntries(
|
|||
if !state.transactionsExpanded {
|
||||
moreCount = min(20, transactionsInfo.count - Int32(transactions.count))
|
||||
} else {
|
||||
moreCount = min(500, transactionsInfo.count - Int32(transactions.count))
|
||||
moreCount = min(50, transactionsInfo.count - Int32(transactions.count))
|
||||
}
|
||||
entries.append(.adsTransactionsExpand(presentationData.theme, presentationData.strings.Monetization_Transaction_ShowMoreTransactions(moreCount)))
|
||||
}
|
||||
}
|
||||
|
||||
if displayStarsTransactions {
|
||||
if !addedTransactionsTabs {
|
||||
entries.append(.adsTransactionsTitle(presentationData.theme, presentationData.strings.Monetization_StarsTransactions.uppercased()))
|
||||
}
|
||||
|
||||
var transactions = starsTransactionsInfo.transactions
|
||||
var limit: Int32
|
||||
if state.transactionsExpanded {
|
||||
limit = 25 + state.moreTransactionsDisplayed
|
||||
} else {
|
||||
limit = initialTransactionsDisplayedLimit
|
||||
}
|
||||
transactions = Array(transactions.prefix(Int(limit)))
|
||||
|
||||
var i: Int32 = 0
|
||||
for transaction in transactions {
|
||||
entries.append(.adsStarsTransaction(i, presentationData.theme, transaction))
|
||||
i += 1
|
||||
}
|
||||
|
||||
if starsTransactionsInfo.canLoadMore || starsTransactionsInfo.transactions.count > transactions.count {
|
||||
let moreCount: Int32
|
||||
if !state.transactionsExpanded {
|
||||
moreCount = min(20, Int32(starsTransactionsInfo.transactions.count - transactions.count))
|
||||
} else {
|
||||
moreCount = min(50, Int32(starsTransactionsInfo.transactions.count - transactions.count))
|
||||
}
|
||||
entries.append(.adsTransactionsExpand(presentationData.theme, presentationData.strings.Monetization_Transaction_ShowMoreTransactions(moreCount)))
|
||||
}
|
||||
|
|
@ -1471,9 +1647,10 @@ private func channelStatsControllerEntries(
|
|||
giveawayAvailable: Bool,
|
||||
isGroup: Bool,
|
||||
boostsOnly: Bool,
|
||||
animatedEmojis: [String: [StickerPackItem]],
|
||||
revenueState: RevenueStats?,
|
||||
revenueTransactions: RevenueStatsTransactionsContext.State,
|
||||
starsState: StarsRevenueStats?,
|
||||
starsTransactions: StarsTransactionsContext.State,
|
||||
adsRestricted: Bool,
|
||||
premiumConfiguration: PremiumConfiguration,
|
||||
monetizationConfiguration: MonetizationConfiguration
|
||||
|
|
@ -1512,8 +1689,9 @@ private func channelStatsControllerEntries(
|
|||
data: revenueState,
|
||||
boostData: boostData,
|
||||
transactionsInfo: revenueTransactions,
|
||||
starsData: starsState,
|
||||
starsTransactionsInfo: starsTransactions,
|
||||
adsRestricted: adsRestricted,
|
||||
animatedEmojis: animatedEmojis,
|
||||
premiumConfiguration: premiumConfiguration,
|
||||
monetizationConfiguration: monetizationConfiguration
|
||||
)
|
||||
|
|
@ -1523,8 +1701,8 @@ private func channelStatsControllerEntries(
|
|||
}
|
||||
|
||||
public func channelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, section: ChannelStatsSection = .stats, boostStatus: ChannelBoostStatus? = nil, boostStatusUpdated: ((ChannelBoostStatus) -> Void)? = nil) -> ViewController {
|
||||
let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0), ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0))
|
||||
let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, starsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0), ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, starsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0))
|
||||
let updateState: ((ChannelStatsControllerState) -> ChannelStatsControllerState) -> Void = { f in
|
||||
statePromise.set(stateValue.modify { f($0) })
|
||||
}
|
||||
|
|
@ -1583,7 +1761,13 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
|
|||
let revenueState = Promise<RevenueStatsContextState?>()
|
||||
revenueState.set(.single(nil) |> then(revenueContext.state |> map(Optional.init)))
|
||||
|
||||
let starsContext = context.engine.payments.peerStarsRevenueContext(peerId: peerId)
|
||||
let starsState = Promise<StarsRevenueStatsContextState?>()
|
||||
starsState.set(.single(nil) |> then(starsContext.state |> map(Optional.init)))
|
||||
|
||||
let revenueTransactions = RevenueStatsTransactionsContext(account: context.account, peerId: peerId)
|
||||
let starsTransactions = context.engine.payments.peerStarsTransactionsContext(subject: .peer(peerId), mode: .all)
|
||||
starsTransactions.loadMore()
|
||||
|
||||
var dismissAllTooltipsImpl: (() -> Void)?
|
||||
var presentImpl: ((ViewController) -> Void)?
|
||||
|
|
@ -1592,8 +1776,10 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
|
|||
var navigateToChatImpl: ((EnginePeer) -> Void)?
|
||||
var navigateToMessageImpl: ((EngineMessage.Id) -> Void)?
|
||||
var openBoostImpl: ((Bool) -> Void)?
|
||||
var openTransactionImpl: ((RevenueStatsTransactionsContext.State.Transaction) -> Void)?
|
||||
var requestWithdrawImpl: (() -> Void)?
|
||||
var openTonTransactionImpl: ((RevenueStatsTransactionsContext.State.Transaction) -> Void)?
|
||||
var openStarsTransactionImpl: ((StarsContext.State.Transaction) -> Void)?
|
||||
var requestTonWithdrawImpl: (() -> Void)?
|
||||
var requestStarsWithdrawImpl: (() -> Void)?
|
||||
var updateStatusBarImpl: ((StatusBarStyle) -> Void)?
|
||||
var dismissInputImpl: (() -> Void)?
|
||||
|
||||
|
|
@ -1716,8 +1902,14 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
|
|||
updateGiftsSelected: { selected in
|
||||
updateState { $0.withUpdatedGiftsSelected(selected).withUpdatedBoostersExpanded(false) }
|
||||
},
|
||||
requestWithdraw: {
|
||||
requestWithdrawImpl?()
|
||||
updateStarsSelected: { selected in
|
||||
updateState { $0.withUpdatedStarsSelected(selected).withUpdatedTransactionsExpanded(false) }
|
||||
},
|
||||
requestTonWithdraw: {
|
||||
requestTonWithdrawImpl?()
|
||||
},
|
||||
requestStarsWithdraw: {
|
||||
requestStarsWithdrawImpl?()
|
||||
},
|
||||
openMonetizationIntro: {
|
||||
let controller = MonetizationIntroScreen(context: context, openMore: {})
|
||||
|
|
@ -1727,18 +1919,27 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
|
|||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: presentationData.strings.Monetization_BalanceInfo_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
|
||||
},
|
||||
openTransaction: { transaction in
|
||||
openTransactionImpl?(transaction)
|
||||
openTonTransaction: { transaction in
|
||||
openTonTransactionImpl?(transaction)
|
||||
},
|
||||
openStarsTransaction: { transaction in
|
||||
openStarsTransactionImpl?(transaction)
|
||||
},
|
||||
expandTransactions: {
|
||||
var starsSelected = false
|
||||
updateState { state in
|
||||
starsSelected = state.starsSelected
|
||||
if state.transactionsExpanded {
|
||||
return state.withUpdatedMoreTransactionsDisplayed(state.moreTransactionsDisplayed + 50)
|
||||
} else {
|
||||
return state.withUpdatedTransactionsExpanded(true)
|
||||
}
|
||||
}
|
||||
revenueTransactions.loadMore()
|
||||
if starsSelected {
|
||||
starsTransactions.loadMore()
|
||||
} else {
|
||||
revenueTransactions.loadMore()
|
||||
}
|
||||
},
|
||||
updateCpmEnabled: { value in
|
||||
let _ = context.engine.peers.updateChannelRestrictAdMessages(peerId: peerId, restricted: value).start()
|
||||
|
|
@ -1802,12 +2003,13 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
|
|||
giftsContext.state,
|
||||
revenueState.get(),
|
||||
revenueTransactions.state,
|
||||
starsState.get(),
|
||||
starsTransactions.state,
|
||||
peerData,
|
||||
longLoadingSignal,
|
||||
context.animatedEmojiStickers
|
||||
longLoadingSignal
|
||||
)
|
||||
|> deliverOnMainQueue
|
||||
|> map { presentationData, state, peer, data, messageView, stories, boostData, boostersState, giftsState, revenueState, revenueTransactions, peerData, longLoading, animatedEmojiStickers -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
|> map { presentationData, state, peer, data, messageView, stories, boostData, boostersState, giftsState, revenueState, revenueTransactions, starsState, starsTransactions, peerData, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let (adsRestricted, canViewRevenue) = peerData
|
||||
|
||||
var isGroup = false
|
||||
|
|
@ -1900,7 +2102,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
|
|||
}
|
||||
|
||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelStatsControllerEntries(presentationData: presentationData, state: state, peer: peer, data: data, messages: messages, stories: stories, interactions: interactions, boostData: boostData, boostersState: boostersState, giftsState: giftsState, giveawayAvailable: premiumConfiguration.giveawayGiftsPurchaseAvailable, isGroup: isGroup, boostsOnly: boostsOnly, animatedEmojis: animatedEmojiStickers, revenueState: revenueState?.stats, revenueTransactions: revenueTransactions, adsRestricted: adsRestricted, premiumConfiguration: premiumConfiguration, monetizationConfiguration: monetizationConfiguration), style: .blocks, emptyStateItem: emptyStateItem, headerItem: headerItem, crossfadeState: previous == nil, animateChanges: false)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelStatsControllerEntries(presentationData: presentationData, state: state, peer: peer, data: data, messages: messages, stories: stories, interactions: interactions, boostData: boostData, boostersState: boostersState, giftsState: giftsState, giveawayAvailable: premiumConfiguration.giveawayGiftsPurchaseAvailable, isGroup: isGroup, boostsOnly: boostsOnly, revenueState: revenueState?.stats, revenueTransactions: revenueTransactions, starsState: starsState?.stats, starsTransactions: starsTransactions, adsRestricted: adsRestricted, premiumConfiguration: premiumConfiguration, monetizationConfiguration: monetizationConfiguration), style: .blocks, emptyStateItem: emptyStateItem, headerItem: headerItem, crossfadeState: previous == nil, animateChanges: false)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|
|
@ -1909,6 +2111,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
|
|||
let _ = statsContext.state
|
||||
let _ = storyList.state
|
||||
let _ = revenueContext.state
|
||||
let _ = starsContext.state
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
|
|
@ -2122,7 +2325,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
|
|||
})
|
||||
}
|
||||
}
|
||||
requestWithdrawImpl = {
|
||||
requestTonWithdrawImpl = {
|
||||
withdrawalDisposable.set((context.engine.peers.checkChannelRevenueWithdrawalAvailability()
|
||||
|> deliverOnMainQueue).start(error: { error in
|
||||
let controller = revenueWithdrawalController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, initialError: error, present: { c, _ in
|
||||
|
|
@ -2134,7 +2337,44 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
|
|||
presentImpl?(controller)
|
||||
}))
|
||||
}
|
||||
openTransactionImpl = { transaction in
|
||||
requestStarsWithdrawImpl = {
|
||||
withdrawalDisposable.set((context.engine.peers.checkStarsRevenueWithdrawalAvailability()
|
||||
|> deliverOnMainQueue).start(error: { error in
|
||||
switch error {
|
||||
case .requestPassword:
|
||||
let _ = (starsContext.state
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).startStandalone(next: { state in
|
||||
guard let stats = state.stats else {
|
||||
return
|
||||
}
|
||||
let controller = context.sharedContext.makeStarsWithdrawalScreen(context: context, stats: stats, completion: { amount in
|
||||
let controller = confirmStarsRevenueWithdrawalController(context: context, peerId: peerId, amount: amount, present: { c, a in
|
||||
presentImpl?(c)
|
||||
}, completion: { url in
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
|
||||
|
||||
Queue.mainQueue().after(2.0) {
|
||||
starsContext.reload()
|
||||
starsTransactions.reload()
|
||||
}
|
||||
})
|
||||
presentImpl?(controller)
|
||||
})
|
||||
pushImpl?(controller)
|
||||
})
|
||||
default:
|
||||
let controller = starsRevenueWithdrawalController(context: context, peerId: peerId, amount: 0, initialError: error, present: { c, a in
|
||||
presentImpl?(c)
|
||||
}, completion: { _ in
|
||||
|
||||
})
|
||||
presentImpl?(controller)
|
||||
}
|
||||
}))
|
||||
}
|
||||
openTonTransactionImpl = { transaction in
|
||||
let _ = (peer.get()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
|
|
@ -2146,6 +2386,16 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
|
|||
}))
|
||||
})
|
||||
}
|
||||
openStarsTransactionImpl = { transaction in
|
||||
let _ = (peer.get()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
guard let peer else {
|
||||
return
|
||||
}
|
||||
pushImpl?(context.sharedContext.makeStarsTransactionScreen(context: context, transaction: transaction, peer: peer))
|
||||
})
|
||||
}
|
||||
updateStatusBarImpl = { [weak controller] style in
|
||||
controller?.setStatusBarStyle(style, animated: true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import TextFormat
|
|||
final class MonetizationBalanceItem: ListViewItem, ItemListItem {
|
||||
let context: AccountContext
|
||||
let presentationData: ItemListPresentationData
|
||||
let stats: RevenueStats
|
||||
let stats: Stats
|
||||
let canWithdraw: Bool
|
||||
let isEnabled: Bool
|
||||
let withdrawAction: () -> Void
|
||||
|
|
@ -23,7 +23,7 @@ final class MonetizationBalanceItem: ListViewItem, ItemListItem {
|
|||
init(
|
||||
context: AccountContext,
|
||||
presentationData: ItemListPresentationData,
|
||||
stats: RevenueStats,
|
||||
stats: Stats,
|
||||
canWithdraw: Bool,
|
||||
isEnabled: Bool,
|
||||
withdrawAction: @escaping () -> Void,
|
||||
|
|
@ -158,13 +158,24 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode {
|
|||
let integralFont = Font.with(size: 48.0, design: .round, weight: .semibold)
|
||||
let fractionalFont = Font.with(size: 24.0, design: .round, weight: .semibold)
|
||||
|
||||
let cryptoValue = formatBalanceText(item.stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator)
|
||||
let amountString: NSAttributedString
|
||||
let value: String
|
||||
|
||||
let amountString = amountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor)
|
||||
var isStars = false
|
||||
if let stats = item.stats as? RevenueStats {
|
||||
let cryptoValue = formatBalanceText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator)
|
||||
amountString = amountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor)
|
||||
value = stats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.availableBalance, rate: stats.usdRate))"
|
||||
} else if let stats = item.stats as? StarsRevenueStats {
|
||||
amountString = NSAttributedString(string: presentationStringsFormattedNumber(Int32(stats.balances.availableBalance), item.presentationData.dateTimeFormat.groupingSeparator), font: integralFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
|
||||
value = stats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.availableBalance, rate: stats.usdRate))"
|
||||
isStars = true
|
||||
} else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
let (balanceLayout, balanceApply) = makeBalanceTextLayout(TextNodeLayoutArguments(attributedString: amountString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let value = item.stats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(item.stats.balances.availableBalance, rate: item.stats.usdRate))"
|
||||
let (valueLayout, valueApply) = makeValueTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: value, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let verticalInset: CGFloat = 13.0
|
||||
|
|
@ -272,7 +283,11 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode {
|
|||
}
|
||||
|
||||
if themeUpdated {
|
||||
strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonBig"), color: item.presentationData.theme.list.itemAccentColor)
|
||||
if isStars {
|
||||
strongSelf.iconNode.image = UIImage(bundleImageName: "Premium/Stars/BalanceStar")
|
||||
} else {
|
||||
strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonBig"), color: item.presentationData.theme.list.itemAccentColor)
|
||||
}
|
||||
}
|
||||
|
||||
var emojiItemSize = CGSize()
|
||||
|
|
@ -304,7 +319,7 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode {
|
|||
strongSelf.addSubnode(withdrawButtonNode)
|
||||
strongSelf.withdrawButtonNode = withdrawButtonNode
|
||||
}
|
||||
withdrawButtonNode.title = item.presentationData.strings.Monetization_BalanceWithdraw
|
||||
withdrawButtonNode.title = isStars ? item.presentationData.strings.Monetization_BalanceStarsWithdraw : item.presentationData.strings.Monetization_BalanceWithdraw
|
||||
withdrawButtonNode.isEnabled = item.isEnabled
|
||||
|
||||
let buttonWidth = contentSize.width - leftInset - rightInset
|
||||
|
|
|
|||
395
submodules/StatisticsUI/Sources/StarsTransactionItem.swift
Normal file
395
submodules/StatisticsUI/Sources/StarsTransactionItem.swift
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import ComponentFlow
|
||||
import ListActionItemComponent
|
||||
import MultilineTextComponent
|
||||
import TelegramStringFormatting
|
||||
import StarsAvatarComponent
|
||||
|
||||
final class StarsTransactionItem: ListViewItem, ItemListItem {
|
||||
let context: AccountContext
|
||||
let presentationData: ItemListPresentationData
|
||||
let transaction: StarsContext.State.Transaction
|
||||
let action: () -> Void
|
||||
let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
presentationData: ItemListPresentationData,
|
||||
transaction: StarsContext.State.Transaction,
|
||||
action: @escaping () -> Void,
|
||||
sectionId: ItemListSectionId,
|
||||
style: ItemListStyle
|
||||
) {
|
||||
self.context = context
|
||||
self.presentationData = presentationData
|
||||
self.transaction = transaction
|
||||
self.action = action
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = StarsTransactionItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? StarsTransactionItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selectable: Bool = true
|
||||
|
||||
public func selected(listView: ListView) {
|
||||
listView.clearHighlightAnimated(true)
|
||||
self.action()
|
||||
}
|
||||
}
|
||||
|
||||
final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private let componentView: ComponentView<Empty>
|
||||
|
||||
private let activateArea: AccessibilityAreaNode
|
||||
|
||||
private var item: StarsTransactionItem?
|
||||
|
||||
var tag: ItemListItemTag? = nil
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
self.componentView = ComponentView<Empty>()
|
||||
|
||||
self.activateArea = AccessibilityAreaNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: StarsTransactionItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
var updatedTheme: PresentationTheme?
|
||||
if currentItem?.presentationData.theme !== item.presentationData.theme {
|
||||
updatedTheme = item.presentationData.theme
|
||||
}
|
||||
|
||||
let contentSize: CGSize
|
||||
let insets: UIEdgeInsets
|
||||
let separatorHeight = UIScreenPixel
|
||||
let itemBackgroundColor: UIColor
|
||||
let itemSeparatorColor: UIColor
|
||||
|
||||
let leftInset = 16.0 + params.leftInset
|
||||
|
||||
let height: CGFloat = 78.0
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
|
||||
itemSeparatorColor = .clear
|
||||
insets = UIEdgeInsets()
|
||||
case .blocks:
|
||||
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
|
||||
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
||||
insets = itemListNeighborsGroupedInsets(neighbors, params)
|
||||
}
|
||||
|
||||
contentSize = CGSize(width: params.width, height: height)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
|
||||
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
|
||||
strongSelf.activateArea.accessibilityTraits = []
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
|
||||
}
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
if strongSelf.backgroundNode.supernode != nil {
|
||||
strongSelf.backgroundNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode != nil {
|
||||
strongSelf.topStripeNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
|
||||
}
|
||||
if strongSelf.maskNode.supernode != nil {
|
||||
strongSelf.maskNode.removeFromSupernode()
|
||||
}
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
|
||||
case .blocks:
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
if strongSelf.maskNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||
}
|
||||
|
||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset
|
||||
strongSelf.bottomStripeNode.isHidden = false
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
}
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
|
||||
}
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
|
||||
|
||||
let fontBaseDisplaySize = 17.0
|
||||
|
||||
let itemTitle: String
|
||||
let itemSubtitle: String?
|
||||
var itemDate: String
|
||||
switch item.transaction.peer {
|
||||
case let .peer(peer):
|
||||
if !item.transaction.media.isEmpty {
|
||||
//TODO:localize
|
||||
itemTitle = "Media Purchase"
|
||||
itemSubtitle = peer.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast)
|
||||
} else if let title = item.transaction.title {
|
||||
itemTitle = title
|
||||
itemSubtitle = peer.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast)
|
||||
} else {
|
||||
itemTitle = peer.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast)
|
||||
itemSubtitle = nil
|
||||
}
|
||||
case .appStore:
|
||||
itemTitle = item.presentationData.strings.Stars_Intro_Transaction_AppleTopUp_Title
|
||||
itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_AppleTopUp_Subtitle
|
||||
case .playMarket:
|
||||
itemTitle = item.presentationData.strings.Stars_Intro_Transaction_GoogleTopUp_Title
|
||||
itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_GoogleTopUp_Subtitle
|
||||
case .fragment:
|
||||
itemTitle = item.presentationData.strings.Stars_Intro_Transaction_FragmentWithdrawal_Title
|
||||
itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_FragmentWithdrawal_Subtitle
|
||||
case .premiumBot:
|
||||
itemTitle = item.presentationData.strings.Stars_Intro_Transaction_PremiumBotTopUp_Title
|
||||
itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_PremiumBotTopUp_Subtitle
|
||||
case .unsupported:
|
||||
itemTitle = item.presentationData.strings.Stars_Intro_Transaction_Unsupported_Title
|
||||
itemSubtitle = nil
|
||||
}
|
||||
|
||||
let itemLabel: NSAttributedString
|
||||
let labelString: String
|
||||
|
||||
let formattedLabel = presentationStringsFormattedNumber(abs(Int32(item.transaction.count)), item.presentationData.dateTimeFormat.groupingSeparator)
|
||||
if item.transaction.count < 0 {
|
||||
labelString = "- \(formattedLabel)"
|
||||
} else {
|
||||
labelString = "+ \(formattedLabel)"
|
||||
}
|
||||
itemLabel = NSAttributedString(string: labelString, font: Font.medium(fontBaseDisplaySize), textColor: labelString.hasPrefix("-") ? item.presentationData.theme.list.itemDestructiveColor : item.presentationData.theme.list.itemDisclosureActions.constructive.fillColor)
|
||||
|
||||
itemDate = stringForMediumCompactDate(timestamp: item.transaction.date, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
|
||||
if item.transaction.flags.contains(.isRefund) {
|
||||
itemDate += " – \(item.presentationData.strings.Stars_Intro_Transaction_Refund)"
|
||||
}
|
||||
|
||||
var titleComponents: [AnyComponentWithIdentity<Empty>] = []
|
||||
titleComponents.append(
|
||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: itemTitle,
|
||||
font: Font.semibold(fontBaseDisplaySize),
|
||||
textColor: item.presentationData.theme.list.itemPrimaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
)))
|
||||
)
|
||||
if let itemSubtitle {
|
||||
titleComponents.append(
|
||||
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: itemSubtitle,
|
||||
font: Font.regular(fontBaseDisplaySize * 16.0 / 17.0),
|
||||
textColor: item.presentationData.theme.list.itemPrimaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
)))
|
||||
)
|
||||
}
|
||||
titleComponents.append(
|
||||
AnyComponentWithIdentity(id: AnyHashable(2), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: itemDate,
|
||||
font: Font.regular(floor(fontBaseDisplaySize * 14.0 / 17.0)),
|
||||
textColor: item.presentationData.theme.list.itemSecondaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
)))
|
||||
)
|
||||
let itemSize = strongSelf.componentView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(ListActionItemComponent(
|
||||
theme: item.presentationData.theme,
|
||||
title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)),
|
||||
contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0),
|
||||
leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: item.context, theme: item.presentationData.theme, peer: item.transaction.peer, photo: item.transaction.photo, media: item.transaction.media))), false),
|
||||
icon: nil,
|
||||
accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))),
|
||||
action: { [weak self] _ in
|
||||
guard let self, let item = self.item else {
|
||||
return
|
||||
}
|
||||
if !item.transaction.flags.contains(.isLocal) {
|
||||
item.action()
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: params.width - params.leftInset - params.rightInset, height: height)
|
||||
)
|
||||
let itemFrame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: itemSize)
|
||||
if let itemComponentView = strongSelf.componentView.view {
|
||||
if itemComponentView.superview == nil {
|
||||
strongSelf.view.addSubview(itemComponentView)
|
||||
}
|
||||
itemComponentView.isUserInteractionEnabled = false
|
||||
itemComponentView.frame = itemFrame
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
|
@ -39,25 +39,29 @@ extension RevenueStats: Stats {
|
|||
|
||||
}
|
||||
|
||||
extension StarsRevenueStats: Stats {
|
||||
|
||||
}
|
||||
|
||||
class StatsOverviewItem: ListViewItem, ItemListItem {
|
||||
let context: AccountContext
|
||||
let presentationData: ItemListPresentationData
|
||||
let isGroup: Bool
|
||||
let stats: Stats
|
||||
let additionalStats: Stats?
|
||||
let storyViews: EngineStoryItem.Views?
|
||||
let publicShares: Int32?
|
||||
let animatedEmoji: TelegramMediaFile?
|
||||
let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
|
||||
init(context: AccountContext, presentationData: ItemListPresentationData, isGroup: Bool, stats: Stats, storyViews: EngineStoryItem.Views? = nil, publicShares: Int32? = nil, animatedEmoji: TelegramMediaFile? = nil, sectionId: ItemListSectionId, style: ItemListStyle) {
|
||||
init(context: AccountContext, presentationData: ItemListPresentationData, isGroup: Bool, stats: Stats, additionalStats: Stats? = nil, storyViews: EngineStoryItem.Views? = nil, publicShares: Int32? = nil, sectionId: ItemListSectionId, style: ItemListStyle) {
|
||||
self.context = context
|
||||
self.presentationData = presentationData
|
||||
self.isGroup = isGroup
|
||||
self.stats = stats
|
||||
self.additionalStats = additionalStats
|
||||
self.storyViews = storyViews
|
||||
self.publicShares = publicShares
|
||||
self.animatedEmoji = animatedEmoji
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
}
|
||||
|
|
@ -99,6 +103,12 @@ class StatsOverviewItem: ListViewItem, ItemListItem {
|
|||
}
|
||||
|
||||
private final class ValueItemNode: ASDisplayNode {
|
||||
enum Mode {
|
||||
case generic
|
||||
case ton
|
||||
case stars
|
||||
}
|
||||
|
||||
enum DeltaColor {
|
||||
case generic
|
||||
case positive
|
||||
|
|
@ -110,6 +120,7 @@ private final class ValueItemNode: ASDisplayNode {
|
|||
private let deltaNode: TextNode
|
||||
private var iconNode: ASImageNode?
|
||||
|
||||
var currentIconName: String?
|
||||
var currentTheme: PresentationTheme?
|
||||
var pressed: (() -> Void)?
|
||||
|
||||
|
|
@ -127,13 +138,13 @@ private final class ValueItemNode: ASDisplayNode {
|
|||
self.addSubnode(self.deltaNode)
|
||||
}
|
||||
|
||||
static func asyncLayout(_ current: ValueItemNode?) -> (_ context: AccountContext, _ width: CGFloat, _ presentationData: ItemListPresentationData, _ value: String, _ title: String, _ delta: (String, DeltaColor)?, _ isTon: Bool) -> (CGSize, () -> ValueItemNode) {
|
||||
static func asyncLayout(_ current: ValueItemNode?) -> (_ context: AccountContext, _ width: CGFloat, _ presentationData: ItemListPresentationData, _ value: String, _ title: String, _ delta: (String, DeltaColor)?, _ mode: Mode) -> (CGSize, () -> ValueItemNode) {
|
||||
|
||||
let maybeMakeValueLayout = (current?.valueNode).flatMap(TextNode.asyncLayout)
|
||||
let maybeMakeTitleLayout = (current?.titleNode).flatMap(TextNode.asyncLayout)
|
||||
let maybeMakeDeltaLayout = (current?.deltaNode).flatMap(TextNode.asyncLayout)
|
||||
|
||||
return { context, width, presentationData, value, title, delta, isTon in
|
||||
return { context, width, presentationData, value, title, delta, mode in
|
||||
let targetNode: ValueItemNode
|
||||
if let current = current {
|
||||
targetNode = current
|
||||
|
|
@ -188,7 +199,7 @@ private final class ValueItemNode: ASDisplayNode {
|
|||
let constrainedSize = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)
|
||||
|
||||
let valueString: NSAttributedString
|
||||
if isTon {
|
||||
if case .ton = mode {
|
||||
valueString = amountAttributedString(value, integralFont: valueFont, fractionalFont: smallValueFont, color: valueColor)
|
||||
} else {
|
||||
valueString = NSAttributedString(string: value, font: valueFont, textColor: valueColor)
|
||||
|
|
@ -214,7 +225,27 @@ private final class ValueItemNode: ASDisplayNode {
|
|||
let _ = deltaApply()
|
||||
|
||||
var valueOffset: CGFloat = 0.0
|
||||
if isTon {
|
||||
let iconName: String?
|
||||
var iconTinted = false
|
||||
switch mode {
|
||||
case .ton:
|
||||
iconName = "Ads/TonMedium"
|
||||
iconTinted = true
|
||||
valueOffset = 17.0
|
||||
case .stars:
|
||||
iconName = "Premium/Stars/StarMedium"
|
||||
valueOffset = 19.0
|
||||
default:
|
||||
iconName = nil
|
||||
}
|
||||
|
||||
var iconNameUpdated = false
|
||||
if targetNode.currentIconName != iconName {
|
||||
targetNode.currentIconName = iconName
|
||||
iconNameUpdated = true
|
||||
}
|
||||
|
||||
if let iconName {
|
||||
let iconNode: ASImageNode
|
||||
if let current = targetNode.iconNode {
|
||||
iconNode = current
|
||||
|
|
@ -225,16 +256,18 @@ private final class ValueItemNode: ASDisplayNode {
|
|||
targetNode.addSubnode(iconNode)
|
||||
}
|
||||
|
||||
if themeUpdated {
|
||||
iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonMedium"), color: presentationData.theme.list.itemAccentColor)
|
||||
if themeUpdated || iconNameUpdated {
|
||||
if iconTinted {
|
||||
iconNode.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: presentationData.theme.list.itemAccentColor)
|
||||
} else {
|
||||
iconNode.image = UIImage(bundleImageName: iconName)
|
||||
}
|
||||
}
|
||||
|
||||
if let icon = iconNode.image {
|
||||
let iconFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((valueLayout.size.height - icon.size.height) / 2.0) - 1.0), size: icon.size)
|
||||
iconNode.frame = iconFrame
|
||||
}
|
||||
|
||||
valueOffset += 17.0
|
||||
} else if let iconNode = targetNode.iconNode {
|
||||
iconNode.removeFromSupernode()
|
||||
targetNode.iconNode = nil
|
||||
|
|
@ -382,7 +415,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
compactNumericCountString(stats.views),
|
||||
item.presentationData.strings.Stats_Message_Views,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
topRightItemLayoutAndApply = makeTopRightItemLayout(
|
||||
|
|
@ -392,7 +425,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "–",
|
||||
item.presentationData.strings.Stats_Message_PublicShares,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
|
||||
|
|
@ -402,7 +435,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
compactNumericCountString(stats.reactions),
|
||||
item.presentationData.strings.Stats_Message_Reactions,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
|
||||
|
|
@ -412,7 +445,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, stats.forwards - Int($0))))" } ?? "–",
|
||||
item.presentationData.strings.Stats_Message_PrivateShares,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
height += topRightItemLayoutAndApply!.0.height * 2.0 + verticalSpacing
|
||||
|
|
@ -424,7 +457,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
compactNumericCountString(views.seenCount),
|
||||
item.presentationData.strings.Stats_Message_Views,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
topRightItemLayoutAndApply = makeTopRightItemLayout(
|
||||
|
|
@ -434,7 +467,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "–",
|
||||
item.presentationData.strings.Stats_Message_PublicShares,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
|
||||
|
|
@ -444,7 +477,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
compactNumericCountString(views.reactedCount),
|
||||
item.presentationData.strings.Stats_Message_Reactions,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
|
||||
|
|
@ -454,7 +487,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, views.forwardCount - Int($0))))" } ?? "–",
|
||||
item.presentationData.strings.Stats_Message_PrivateShares,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
height += topRightItemLayoutAndApply!.0.height * 2.0 + verticalSpacing
|
||||
|
|
@ -466,7 +499,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
"\(stats.level)",
|
||||
item.presentationData.strings.Stats_Boosts_Level,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
var premiumSubscribers: Double = 0.0
|
||||
|
|
@ -481,7 +514,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
"≈\(Int(stats.premiumAudience?.value ?? 0))",
|
||||
item.isGroup ? item.presentationData.strings.Stats_Boosts_PremiumMembers : item.presentationData.strings.Stats_Boosts_PremiumSubscribers,
|
||||
(String(format: "%.02f%%", premiumSubscribers * 100.0), .generic),
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
|
||||
|
|
@ -491,7 +524,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
"\(stats.boosts)",
|
||||
item.presentationData.strings.Stats_Boosts_ExistingBoosts,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
let boostsLeft: Int32
|
||||
|
|
@ -507,7 +540,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
"\(boostsLeft)",
|
||||
item.presentationData.strings.Stats_Boosts_BoostsToLevelUp,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
if twoColumnLayout {
|
||||
|
|
@ -532,7 +565,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
compactNumericCountString(Int(stats.followers.current)),
|
||||
item.presentationData.strings.Stats_Followers,
|
||||
(followersDelta.text, followersDelta.positive ? .positive : .negative),
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
var enabledNotifications: Double = 0.0
|
||||
|
|
@ -546,7 +579,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
String(format: "%.02f%%", enabledNotifications * 100.0),
|
||||
item.presentationData.strings.Stats_EnabledNotifications,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
let hasMessages = stats.viewsPerPost.current > 0 || viewsPerPostDelta.hasValue
|
||||
|
|
@ -605,7 +638,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
value,
|
||||
title,
|
||||
delta,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
}
|
||||
if let (value, title, delta) = items[1] {
|
||||
|
|
@ -616,7 +649,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
value,
|
||||
title,
|
||||
delta,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
}
|
||||
if let (value, title, delta) = items[2] {
|
||||
|
|
@ -627,7 +660,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
value,
|
||||
title,
|
||||
delta,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
}
|
||||
if let (value, title, delta) = items[3] {
|
||||
|
|
@ -638,7 +671,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
value,
|
||||
title,
|
||||
delta,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
}
|
||||
if let (value, title, delta) = items[4] {
|
||||
|
|
@ -649,7 +682,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
value,
|
||||
title,
|
||||
delta,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
}
|
||||
if let (value, title, delta) = items[5] {
|
||||
|
|
@ -660,7 +693,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
value,
|
||||
title,
|
||||
delta,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -684,7 +717,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
compactNumericCountString(Int(stats.members.current)),
|
||||
item.presentationData.strings.Stats_GroupMembers,
|
||||
(membersDelta.text, membersDelta.positive ? .positive : .negative),
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
let messagesDelta = deltaText(stats.messages)
|
||||
|
|
@ -695,7 +728,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
compactNumericCountString(Int(stats.messages.current)),
|
||||
item.presentationData.strings.Stats_GroupMessages,
|
||||
(messagesDelta.text, messagesDelta.positive ? .positive : .negative),
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
if displayBottomRow {
|
||||
|
|
@ -706,7 +739,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
compactNumericCountString(Int(stats.viewers.current)),
|
||||
item.presentationData.strings.Stats_GroupViewers,
|
||||
(viewersDelta.text, viewersDelta.positive ? .positive : .negative),
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
|
||||
|
|
@ -716,7 +749,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
compactNumericCountString(Int(stats.posters.current)),
|
||||
item.presentationData.strings.Stats_GroupPosters,
|
||||
(postersDelta.text, postersDelta.positive ? .positive : .negative),
|
||||
false
|
||||
.generic
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -726,39 +759,105 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
height += topLeftItemLayoutAndApply!.0.height * 4.0 + verticalSpacing * 3.0
|
||||
}
|
||||
} else if let stats = item.stats as? RevenueStats {
|
||||
twoColumnLayout = false
|
||||
|
||||
topLeftItemLayoutAndApply = makeTopLeftItemLayout(
|
||||
item.context,
|
||||
params.width,
|
||||
item.presentationData,
|
||||
formatBalanceText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator),
|
||||
item.presentationData.strings.Monetization_Overview_Available,
|
||||
(stats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.availableBalance, rate: stats.usdRate))", .generic),
|
||||
true
|
||||
)
|
||||
|
||||
topRightItemLayoutAndApply = makeTopRightItemLayout(
|
||||
item.context,
|
||||
params.width,
|
||||
item.presentationData,
|
||||
formatBalanceText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator),
|
||||
item.presentationData.strings.Monetization_Overview_Current,
|
||||
(stats.balances.currentBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.currentBalance, rate: stats.usdRate))", .generic),
|
||||
true
|
||||
)
|
||||
|
||||
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
|
||||
item.context,
|
||||
params.width,
|
||||
item.presentationData,
|
||||
formatBalanceText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator),
|
||||
item.presentationData.strings.Monetization_Overview_Total,
|
||||
(stats.balances.overallRevenue == 0 ? "" : "≈\(formatUsdValue(stats.balances.overallRevenue, rate: stats.usdRate))", .generic),
|
||||
true
|
||||
)
|
||||
|
||||
height += topLeftItemLayoutAndApply!.0.height * 3.0 + verticalSpacing * 2.0
|
||||
if let additionalStats = item.additionalStats as? StarsRevenueStats, additionalStats.balances.overallRevenue > 0 {
|
||||
twoColumnLayout = true
|
||||
|
||||
topLeftItemLayoutAndApply = makeTopLeftItemLayout(
|
||||
item.context,
|
||||
params.width,
|
||||
item.presentationData,
|
||||
formatBalanceText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator),
|
||||
"Available Balance", //item.presentationData.strings.Monetization_Overview_Available,
|
||||
(stats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.availableBalance, rate: stats.usdRate))", .generic),
|
||||
.ton
|
||||
)
|
||||
|
||||
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
|
||||
item.context,
|
||||
params.width,
|
||||
item.presentationData,
|
||||
formatBalanceText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator),
|
||||
"Current Balance", //item.presentationData.strings.Monetization_Overview_Current,
|
||||
(stats.balances.currentBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.currentBalance, rate: stats.usdRate))", .generic),
|
||||
.ton
|
||||
)
|
||||
|
||||
middle2LeftItemLayoutAndApply = makeMiddle2LeftItemLayout(
|
||||
item.context,
|
||||
params.width,
|
||||
item.presentationData,
|
||||
formatBalanceText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator),
|
||||
"Lifetime Proceeds",//item.presentationData.strings.Monetization_Overview_Total,
|
||||
(stats.balances.overallRevenue == 0 ? "" : "≈\(formatUsdValue(stats.balances.overallRevenue, rate: stats.usdRate))", .generic),
|
||||
.ton
|
||||
)
|
||||
|
||||
topRightItemLayoutAndApply = makeTopRightItemLayout(
|
||||
item.context,
|
||||
params.width,
|
||||
item.presentationData,
|
||||
presentationStringsFormattedNumber(Int32(additionalStats.balances.availableBalance), item.presentationData.dateTimeFormat.groupingSeparator),
|
||||
" ",
|
||||
(additionalStats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(additionalStats.balances.availableBalance, rate: additionalStats.usdRate))", .generic),
|
||||
.stars
|
||||
)
|
||||
|
||||
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
|
||||
item.context,
|
||||
params.width,
|
||||
item.presentationData,
|
||||
presentationStringsFormattedNumber(Int32(additionalStats.balances.currentBalance), item.presentationData.dateTimeFormat.groupingSeparator),
|
||||
" ",
|
||||
(additionalStats.balances.currentBalance == 0 ? "" : "≈\(formatUsdValue(additionalStats.balances.currentBalance, rate: additionalStats.usdRate))", .generic),
|
||||
.stars
|
||||
)
|
||||
|
||||
middle2RightItemLayoutAndApply = makeMiddle2RightItemLayout(
|
||||
item.context,
|
||||
params.width,
|
||||
item.presentationData,
|
||||
presentationStringsFormattedNumber(Int32(additionalStats.balances.overallRevenue), item.presentationData.dateTimeFormat.groupingSeparator),
|
||||
" ",
|
||||
(additionalStats.balances.overallRevenue == 0 ? "" : "≈\(formatUsdValue(additionalStats.balances.overallRevenue, rate: additionalStats.usdRate))", .generic),
|
||||
.stars
|
||||
)
|
||||
|
||||
height += topLeftItemLayoutAndApply!.0.height * 3.0 + verticalSpacing * 2.0
|
||||
} else {
|
||||
twoColumnLayout = false
|
||||
|
||||
topLeftItemLayoutAndApply = makeTopLeftItemLayout(
|
||||
item.context,
|
||||
params.width,
|
||||
item.presentationData,
|
||||
formatBalanceText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator),
|
||||
item.presentationData.strings.Monetization_Overview_Available,
|
||||
(stats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.availableBalance, rate: stats.usdRate))", .generic),
|
||||
.ton
|
||||
)
|
||||
|
||||
topRightItemLayoutAndApply = makeTopRightItemLayout(
|
||||
item.context,
|
||||
params.width,
|
||||
item.presentationData,
|
||||
formatBalanceText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator),
|
||||
item.presentationData.strings.Monetization_Overview_Current,
|
||||
(stats.balances.currentBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.currentBalance, rate: stats.usdRate))", .generic),
|
||||
.ton
|
||||
)
|
||||
|
||||
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
|
||||
item.context,
|
||||
params.width,
|
||||
item.presentationData,
|
||||
formatBalanceText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator),
|
||||
item.presentationData.strings.Monetization_Overview_Total,
|
||||
(stats.balances.overallRevenue == 0 ? "" : "≈\(formatUsdValue(stats.balances.overallRevenue, rate: stats.usdRate))", .generic),
|
||||
.ton
|
||||
)
|
||||
|
||||
height += topLeftItemLayoutAndApply!.0.height * 3.0 + verticalSpacing * 2.0
|
||||
}
|
||||
}
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: height)
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ private func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id
|
|||
|
||||
var parsedTransactions: [StarsContext.State.Transaction] = []
|
||||
for entry in history {
|
||||
if let parsedTransaction = StarsContext.State.Transaction(apiTransaction: entry, transaction: transaction) {
|
||||
if let parsedTransaction = StarsContext.State.Transaction(apiTransaction: entry, peerId: peerId != account.peerId ? peerId : nil, transaction: transaction) {
|
||||
parsedTransactions.append(parsedTransaction)
|
||||
}
|
||||
}
|
||||
|
|
@ -236,7 +236,7 @@ private final class StarsContextImpl {
|
|||
}
|
||||
|
||||
private extension StarsContext.State.Transaction {
|
||||
init?(apiTransaction: Api.StarsTransaction, transaction: Transaction) {
|
||||
init?(apiTransaction: Api.StarsTransaction, peerId: EnginePeer.Id?, transaction: Transaction) {
|
||||
switch apiTransaction {
|
||||
case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia):
|
||||
let parsedPeer: StarsContext.State.Transaction.Peer
|
||||
|
|
@ -258,7 +258,11 @@ private extension StarsContext.State.Transaction {
|
|||
}
|
||||
parsedPeer = .peer(EnginePeer(peer))
|
||||
if let messageId {
|
||||
paidMessageId = MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: messageId)
|
||||
if let peerId {
|
||||
paidMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: messageId)
|
||||
} else {
|
||||
paidMessageId = MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: messageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -526,6 +526,10 @@ public extension Message {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var paidContent: TelegramMediaPaidContent? {
|
||||
return self.media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent
|
||||
}
|
||||
}
|
||||
|
||||
public extension Message {
|
||||
|
|
|
|||
|
|
@ -450,6 +450,8 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/Stars/StarsTransactionsScreen",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsPurchaseScreen",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsTransferScreen",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsTransactionScreen",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen",
|
||||
"//submodules/TelegramUI/Components/Chat/FactCheckAlertController",
|
||||
"//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController",
|
||||
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageUnlockMediaNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageStarsMediaInfoNode",
|
||||
"//submodules/UIKitRuntimeUtils",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode",
|
||||
"//submodules/AnimatedStickerNode",
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ import ChatMessageGiftBubbleContentNode
|
|||
import ChatMessageGiveawayBubbleContentNode
|
||||
import ChatMessageJoinedChannelBubbleContentNode
|
||||
import ChatMessageFactCheckBubbleContentNode
|
||||
import ChatMessageUnlockMediaNode
|
||||
import ChatMessageStarsMediaInfoNode
|
||||
import UIKitRuntimeUtils
|
||||
import ChatMessageTransitionNode
|
||||
import AnimatedStickerNode
|
||||
|
|
@ -134,7 +136,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
|||
if let media = media as? TelegramMediaPaidContent {
|
||||
var index = 0
|
||||
for _ in media.extendedMedia {
|
||||
result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(index: index, isAttachment: true, neighborType: .media, neighborSpacing: .default)))
|
||||
result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(index: index, isAttachment: false, neighborType: .media, neighborSpacing: .default)))
|
||||
index += 1
|
||||
}
|
||||
} else if let _ = media as? TelegramMediaImage {
|
||||
|
|
@ -636,6 +638,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
private var actionButtonsNode: ChatMessageActionButtonsNode?
|
||||
private var reactionButtonsNode: ChatMessageReactionButtonsNode?
|
||||
|
||||
private var unlockButtonNode: ChatMessageUnlockMediaNode?
|
||||
private var mediaInfoNode: ChatMessageStarsMediaInfoNode?
|
||||
|
||||
private var shareButtonNode: ChatMessageShareButton?
|
||||
|
||||
private let messageAccessibilityArea: AccessibilityAreaNode
|
||||
|
|
@ -680,6 +685,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
replyInfoNode.visibility = self.visibility != .none
|
||||
}
|
||||
|
||||
if let unlockButtonNode = self.unlockButtonNode {
|
||||
unlockButtonNode.visibility = self.visibility != .none
|
||||
}
|
||||
|
||||
self.visibilityStatus = self.visibility != .none
|
||||
|
||||
self.updateVisibility()
|
||||
|
|
@ -1198,6 +1207,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
if let replyInfoNode = strongSelf.replyInfoNode, replyInfoNode.frame.contains(point) {
|
||||
return .waitForSingleTap
|
||||
}
|
||||
if let unlockButtonNode = strongSelf.unlockButtonNode, unlockButtonNode.frame.contains(point) {
|
||||
if let _ = unlockButtonNode.hitTest(strongSelf.view.convert(point, to: unlockButtonNode.view), with: nil) {
|
||||
return .fail
|
||||
}
|
||||
}
|
||||
if let forwardInfoNode = strongSelf.forwardInfoNode, forwardInfoNode.frame.contains(point) {
|
||||
if forwardInfoNode.hasAction(at: strongSelf.view.convert(point, to: forwardInfoNode.view)) {
|
||||
return .fail
|
||||
|
|
@ -1366,6 +1380,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
let replyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode)
|
||||
let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode)
|
||||
let reactionButtonsLayout = ChatMessageReactionButtonsNode.asyncLayout(self.reactionButtonsNode)
|
||||
let unlockButtonLayout = ChatMessageUnlockMediaNode.asyncLayout(self.unlockButtonNode)
|
||||
let mediaInfoLayout = ChatMessageStarsMediaInfoNode.asyncLayout(self.mediaInfoNode)
|
||||
|
||||
let mosaicStatusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.mosaicStatusNode)
|
||||
|
||||
|
|
@ -1391,6 +1407,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
replyInfoLayout: replyInfoLayout,
|
||||
actionButtonsLayout: actionButtonsLayout,
|
||||
reactionButtonsLayout: reactionButtonsLayout,
|
||||
unlockButtonLayout: unlockButtonLayout,
|
||||
mediaInfoLayout: mediaInfoLayout,
|
||||
mosaicStatusLayout: mosaicStatusLayout,
|
||||
layoutConstants: layoutConstants,
|
||||
currentItem: currentItem,
|
||||
|
|
@ -1412,6 +1430,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
replyInfoLayout: (ChatMessageReplyInfoNode.Arguments) -> (CGSize, (CGSize, Bool, ListViewItemUpdateAnimation) -> ChatMessageReplyInfoNode),
|
||||
actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, WallpaperBackgroundNode?, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)),
|
||||
reactionButtonsLayout: (ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)),
|
||||
unlockButtonLayout: (ChatMessageUnlockMediaNode.Arguments) -> (CGSize, (Bool) -> ChatMessageUnlockMediaNode),
|
||||
mediaInfoLayout: (ChatMessageStarsMediaInfoNode.Arguments) -> (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode),
|
||||
mosaicStatusLayout: (ChatMessageDateAndStatusNode.Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)),
|
||||
layoutConstants: ChatMessageItemLayoutConstants,
|
||||
currentItem: ChatMessageItem?,
|
||||
|
|
@ -2185,7 +2205,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
}
|
||||
|
||||
if case .customChatContents = item.associatedData.subject {
|
||||
} else if (mosaicRange.upperBound == contentPropertiesAndLayouts.count || contentPropertiesAndLayouts[contentPropertiesAndLayouts.count - 1].3.isAttachment) && !hasText {
|
||||
} else if (mosaicRange.upperBound == contentPropertiesAndLayouts.count || contentPropertiesAndLayouts[contentPropertiesAndLayouts.count - 1].3.isAttachment) && (!hasText || item.message.invertMedia) {
|
||||
let message = item.content.firstMessage
|
||||
|
||||
var edited = false
|
||||
|
|
@ -2286,6 +2306,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
var forwardSource: Peer?
|
||||
var forwardAuthorSignature: String?
|
||||
|
||||
var unlockButtonSizeApply: (CGSize, (Bool) -> ChatMessageUnlockMediaNode?) = (CGSize(), { _ in nil })
|
||||
var mediaInfoSizeApply: (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode?) = (CGSize(), { _ in nil })
|
||||
|
||||
if displayHeader {
|
||||
let bubbleWidthInsets: CGFloat = mosaicRange == nil ? layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right : 0.0
|
||||
if authorNameString != nil || inlineBotNameString != nil {
|
||||
|
|
@ -2824,9 +2847,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
var detachedContentNodesHeight: CGFloat = 0.0
|
||||
|
||||
var mosaicStatusOrigin: CGPoint?
|
||||
var unlockButtonPosition: CGPoint?
|
||||
var mediaInfoOrigin: CGPoint?
|
||||
for i in 0 ..< contentNodePropertiesAndFinalize.count {
|
||||
let (properties, position, finalize, contentGroupId, itemSelection) = contentNodePropertiesAndFinalize[i]
|
||||
|
||||
|
||||
if let position = position, case let .linear(top, bottom) = position {
|
||||
if case let .Neighbour(_, _, spacing) = top, case let .overlap(overlap) = spacing {
|
||||
currentContainerGroupOverlap = overlap
|
||||
|
|
@ -2851,12 +2876,17 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
contentNodeFramesPropertiesAndApply.append((contentNodeFrame, properties, true, apply))
|
||||
|
||||
if i == mosaicRange.upperBound - 1 {
|
||||
unlockButtonPosition = CGPoint(x: size.width / 2.0, y: contentNodesHeight + size.height / 2.0)
|
||||
mediaInfoOrigin = CGPoint(x: size.width, y: contentNodesHeight)
|
||||
|
||||
contentNodesHeight += size.height
|
||||
totalContentNodesHeight += size.height
|
||||
|
||||
|
||||
mosaicStatusOrigin = contentNodeFrame.bottomRight
|
||||
}
|
||||
} else {
|
||||
let contentProperties = contentPropertiesAndLayouts[i].3
|
||||
|
||||
if i == 0 && !headerSize.height.isZero {
|
||||
if contentGroupId == nil {
|
||||
contentNodesHeight += properties.headerSpacing
|
||||
|
|
@ -2870,7 +2900,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
if !contentContainerNodeFrames.isEmpty {
|
||||
overlapOffset = currentContainerGroupOverlap
|
||||
}
|
||||
contentContainerNodeFrames.append((containerGroupId, CGRect(x: 0.0, y: headerSize.height + totalContentNodesHeight - contentNodesHeight - overlapOffset, width: maxContentWidth, height: contentNodesHeight), currentItemSelection, currentContainerGroupOverlap))
|
||||
let containerFrame = CGRect(x: 0.0, y: headerSize.height + totalContentNodesHeight - contentNodesHeight - overlapOffset, width: maxContentWidth, height: contentNodesHeight)
|
||||
contentContainerNodeFrames.append((containerGroupId, containerFrame, currentItemSelection, currentContainerGroupOverlap))
|
||||
|
||||
if !overlapOffset.isZero {
|
||||
totalContentNodesHeight -= currentContainerGroupOverlap
|
||||
}
|
||||
|
|
@ -2885,7 +2917,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
|
||||
let contentNodeOriginY = contentNodesHeight - detachedContentNodesHeight
|
||||
let (size, apply) = finalize(maxContentWidth)
|
||||
contentNodeFramesPropertiesAndApply.append((CGRect(origin: CGPoint(x: 0.0, y: contentNodeOriginY), size: size), properties, contentGroupId == nil, apply))
|
||||
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentNodeOriginY), size: size)
|
||||
contentNodeFramesPropertiesAndApply.append((containerFrame, properties, contentGroupId == nil, apply))
|
||||
|
||||
if contentProperties.neighborType == .media && unlockButtonPosition == nil {
|
||||
unlockButtonPosition = containerFrame.center
|
||||
mediaInfoOrigin = CGPoint(x: containerFrame.width, y: containerFrame.minY)
|
||||
}
|
||||
|
||||
contentNodesHeight += size.height
|
||||
totalContentNodesHeight += size.height
|
||||
|
|
@ -2909,6 +2947,34 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
|
||||
contentSize.height += totalContentNodesHeight
|
||||
|
||||
if let paidContent = item.message.media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent, let media = paidContent.extendedMedia.first {
|
||||
if case .preview = media {
|
||||
let sizeAndApply = unlockButtonLayout(ChatMessageUnlockMediaNode.Arguments(
|
||||
presentationData: item.presentationData,
|
||||
strings: item.presentationData.strings,
|
||||
context: item.context,
|
||||
controllerInteraction: item.controllerInteraction,
|
||||
message: item.message,
|
||||
media: paidContent,
|
||||
constrainedSize: CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude),
|
||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
|
||||
))
|
||||
unlockButtonSizeApply = (sizeAndApply.0, { synchronousLoads in sizeAndApply.1(synchronousLoads) })
|
||||
} else {
|
||||
let sizeAndApply = mediaInfoLayout(ChatMessageStarsMediaInfoNode.Arguments(
|
||||
presentationData: item.presentationData,
|
||||
context: item.context,
|
||||
message: item.message,
|
||||
media: paidContent,
|
||||
constrainedSize: CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude),
|
||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
|
||||
))
|
||||
mediaInfoSizeApply = (sizeAndApply.0, { synchronousLoads in sizeAndApply.1(synchronousLoads) })
|
||||
}
|
||||
}
|
||||
|
||||
var actionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)?
|
||||
if let actionButtonsFinalize = actionButtonsFinalize {
|
||||
actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth)
|
||||
|
|
@ -3039,6 +3105,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
contentContainerNodeFrames: contentContainerNodeFrames,
|
||||
mosaicStatusOrigin: mosaicStatusOrigin,
|
||||
mosaicStatusSizeAndApply: mosaicStatusSizeAndApply,
|
||||
unlockButtonPosition: unlockButtonPosition,
|
||||
unlockButtonSizeAndApply: unlockButtonSizeApply,
|
||||
mediaInfoOrigin: mediaInfoOrigin,
|
||||
mediaInfoSizeAndApply: mediaInfoSizeApply,
|
||||
needsShareButton: needsShareButton,
|
||||
shareButtonOffset: shareButtonOffset,
|
||||
avatarOffset: avatarOffset,
|
||||
|
|
@ -3093,6 +3163,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
contentContainerNodeFrames: [(UInt32, CGRect, Bool?, CGFloat)],
|
||||
mosaicStatusOrigin: CGPoint?,
|
||||
mosaicStatusSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)?,
|
||||
unlockButtonPosition: CGPoint?,
|
||||
unlockButtonSizeAndApply: (CGSize, (Bool) -> ChatMessageUnlockMediaNode?),
|
||||
mediaInfoOrigin: CGPoint?,
|
||||
mediaInfoSizeAndApply: (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode?),
|
||||
needsShareButton: Bool,
|
||||
shareButtonOffset: CGPoint?,
|
||||
avatarOffset: CGFloat?,
|
||||
|
|
@ -4096,6 +4170,56 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
strongSelf.mosaicStatusNode = nil
|
||||
mosaicStatusNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if let unlockButtonPosition {
|
||||
let (size, apply) = unlockButtonSizeAndApply
|
||||
var unlockButtonNodeAnimation = animation
|
||||
if strongSelf.unlockButtonNode == nil {
|
||||
unlockButtonNodeAnimation = .None
|
||||
}
|
||||
let unlockButtonNode = apply(strongSelf.unlockButtonNode != nil)
|
||||
if unlockButtonNode !== strongSelf.unlockButtonNode {
|
||||
strongSelf.unlockButtonNode?.removeFromSupernode()
|
||||
strongSelf.unlockButtonNode = unlockButtonNode
|
||||
if let unlockButtonNode {
|
||||
strongSelf.clippingNode.addSubnode(unlockButtonNode)
|
||||
}
|
||||
}
|
||||
let absoluteOrigin = unlockButtonPosition.offsetBy(dx: contentOrigin.x, dy: contentOrigin.y)
|
||||
if let unlockButtonNode {
|
||||
unlockButtonNodeAnimation.animator.updateFrame(layer: unlockButtonNode.layer, frame: CGRect(origin: CGPoint(x: floor(absoluteOrigin.x - size.width / 2.0), y: floor(absoluteOrigin.y - size.height / 2.0)), size: size), completion: nil)
|
||||
}
|
||||
} else if let unlockButtonNode = strongSelf.unlockButtonNode {
|
||||
strongSelf.unlockButtonNode = nil
|
||||
unlockButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
unlockButtonNode.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
|
||||
if let mediaInfoOrigin {
|
||||
let (size, apply) = mediaInfoSizeAndApply
|
||||
var unlockButtonNodeAnimation = animation
|
||||
if strongSelf.unlockButtonNode == nil {
|
||||
unlockButtonNodeAnimation = .None
|
||||
}
|
||||
let mediaInfoNode = apply(strongSelf.mediaInfoNode != nil)
|
||||
if mediaInfoNode !== strongSelf.mediaInfoNode {
|
||||
strongSelf.mediaInfoNode?.removeFromSupernode()
|
||||
strongSelf.mediaInfoNode = mediaInfoNode
|
||||
if let mediaInfoNode {
|
||||
strongSelf.clippingNode.addSubnode(mediaInfoNode)
|
||||
}
|
||||
}
|
||||
let absoluteOrigin = mediaInfoOrigin.offsetBy(dx: contentOrigin.x, dy: contentOrigin.y)
|
||||
if let mediaInfoNode {
|
||||
unlockButtonNodeAnimation.animator.updateFrame(layer: mediaInfoNode.layer, frame: CGRect(origin: CGPoint(x: absoluteOrigin.x - size.width - 8.0, y: absoluteOrigin.y + 8.0), size: size), completion: nil)
|
||||
}
|
||||
} else if let mediaInfoNode = strongSelf.mediaInfoNode {
|
||||
strongSelf.mediaInfoNode = nil
|
||||
mediaInfoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
mediaInfoNode.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
|
||||
if needsShareButton {
|
||||
if strongSelf.shareButtonNode == nil {
|
||||
|
|
@ -5157,6 +5281,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
}
|
||||
|
||||
override public func updateHiddenMedia() {
|
||||
var hasHiddenMediaInfo = false
|
||||
var hasHiddenMosaicStatus = false
|
||||
var hasHiddenBackground = false
|
||||
if let item = self.item {
|
||||
|
|
@ -5169,6 +5294,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
if let mosaicStatusNode = self.mosaicStatusNode, mosaicStatusNode.frame.intersects(contentNode.frame) {
|
||||
hasHiddenMosaicStatus = true
|
||||
}
|
||||
if let mediaInfoNode = self.mediaInfoNode, mediaInfoNode.frame.intersects(contentNode.frame) {
|
||||
hasHiddenMediaInfo = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5185,6 +5313,17 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
}
|
||||
}
|
||||
|
||||
if let mediaInfoNode = self.mediaInfoNode {
|
||||
if mediaInfoNode.alpha.isZero != hasHiddenMediaInfo {
|
||||
if hasHiddenMediaInfo {
|
||||
mediaInfoNode.alpha = 0.0
|
||||
} else {
|
||||
mediaInfoNode.alpha = 1.0
|
||||
mediaInfoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.backgroundNode.isHidden = hasHiddenBackground
|
||||
self.backgroundWallpaperNode.isHidden = hasHiddenBackground
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2386,7 +2386,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
|
||||
var viewText: String = ""
|
||||
if message.isAgeRestricted() {
|
||||
//TODO: localize
|
||||
//TODO:localize
|
||||
viewText = "18+ Content"
|
||||
} else {
|
||||
outer: for attribute in message.attributes {
|
||||
|
|
@ -2403,9 +2403,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
}
|
||||
}
|
||||
}
|
||||
if let paidContent {
|
||||
viewText = "⭐️\(paidContent.amount)"
|
||||
}
|
||||
// if let paidContent {
|
||||
// viewText = "⭐️\(paidContent.amount)"
|
||||
// }
|
||||
self.extendedMediaOverlayNode?.update(size: self.imageNode.frame.size, text: viewText, imageSignal: self.currentBlurredImageSignal, imageFrame: self.imageNode.view.convert(self.imageNode.bounds, to: self.extendedMediaOverlayNode?.view), corners: self.currentImageArguments?.corners)
|
||||
} else if let extendedMediaOverlayNode = self.extendedMediaOverlayNode {
|
||||
self.extendedMediaOverlayNode = nil
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
case .automaticPlayback:
|
||||
openChatMessageMode = .automaticPlayback
|
||||
}
|
||||
let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: openChatMessageMode))
|
||||
let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: openChatMessageMode, mediaIndex: self.mediaIndex))
|
||||
}
|
||||
|
||||
self.interactiveImageNode.activateAgeRestrictedMedia = { [weak self] in
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatMessageStarsMediaInfoNode",
|
||||
module_name = "ChatMessageStarsMediaInfoNode",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/Display",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/LocalizedPeerData",
|
||||
"//submodules/PhotoResources",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/TelegramUI/Components/TextNodeWithEntities",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/WallpaperBackgroundNode",
|
||||
"//submodules/TelegramUI/Components/ChatControllerInteraction",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import Display
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import LocalizedPeerData
|
||||
import PhotoResources
|
||||
import TelegramStringFormatting
|
||||
import TextFormat
|
||||
import TextNodeWithEntities
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import ComponentFlow
|
||||
import ChatControllerInteraction
|
||||
|
||||
private func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat) -> (CGPoint, UIImage?) {
|
||||
enum CornerType {
|
||||
case topLeft
|
||||
case topRight
|
||||
case bottomLeft
|
||||
case bottomRight
|
||||
}
|
||||
|
||||
func drawFullCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) {
|
||||
if radius.isZero {
|
||||
return
|
||||
}
|
||||
context.setFillColor(color.cgColor)
|
||||
switch type {
|
||||
case .topLeft:
|
||||
context.clear(CGRect(origin: point, size: CGSize(width: radius, height: radius)))
|
||||
context.fillEllipse(in: CGRect(origin: point, size: CGSize(width: radius * 2.0, height: radius * 2.0)))
|
||||
case .topRight:
|
||||
context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
|
||||
case .bottomLeft:
|
||||
context.clear(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
|
||||
case .bottomRight:
|
||||
context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
|
||||
}
|
||||
}
|
||||
|
||||
func drawConnectingCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) {
|
||||
context.setFillColor(color.cgColor)
|
||||
switch type {
|
||||
case .topLeft:
|
||||
context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius)))
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
|
||||
case .topRight:
|
||||
context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius, height: radius)))
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
|
||||
case .bottomLeft:
|
||||
context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius)))
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
|
||||
case .bottomRight:
|
||||
context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius)))
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
|
||||
}
|
||||
}
|
||||
|
||||
if rects.isEmpty {
|
||||
return (CGPoint(), nil)
|
||||
}
|
||||
|
||||
var topLeft = rects[0].origin
|
||||
var bottomRight = CGPoint(x: rects[0].maxX, y: rects[0].maxY)
|
||||
for i in 1 ..< rects.count {
|
||||
topLeft.x = min(topLeft.x, rects[i].origin.x)
|
||||
topLeft.y = min(topLeft.y, rects[i].origin.y)
|
||||
bottomRight.x = max(bottomRight.x, rects[i].maxX)
|
||||
bottomRight.y = max(bottomRight.y, rects[i].maxY)
|
||||
}
|
||||
|
||||
topLeft.x -= inset
|
||||
topLeft.y -= inset
|
||||
bottomRight.x += inset * 2.0
|
||||
bottomRight.y += inset * 2.0
|
||||
|
||||
return (topLeft, generateImage(CGSize(width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(color.cgColor)
|
||||
|
||||
context.setBlendMode(.copy)
|
||||
|
||||
for i in 0 ..< rects.count {
|
||||
let rect = rects[i].insetBy(dx: -inset, dy: -inset)
|
||||
context.fill(rect.offsetBy(dx: -topLeft.x, dy: -topLeft.y))
|
||||
}
|
||||
|
||||
for i in 0 ..< rects.count {
|
||||
let rect = rects[i].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
|
||||
|
||||
var previous: CGRect?
|
||||
if i != 0 {
|
||||
previous = rects[i - 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
|
||||
}
|
||||
|
||||
var next: CGRect?
|
||||
if i != rects.count - 1 {
|
||||
next = rects[i + 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
|
||||
}
|
||||
|
||||
if let previous = previous {
|
||||
if previous.contains(rect.topLeft) {
|
||||
if abs(rect.topLeft.x - previous.minX) >= innerRadius {
|
||||
var radius = innerRadius
|
||||
if let next = next {
|
||||
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
|
||||
}
|
||||
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topLeft.x, y: previous.maxY), type: .topLeft, radius: radius)
|
||||
}
|
||||
} else {
|
||||
drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius)
|
||||
}
|
||||
if previous.contains(rect.topRight.offsetBy(dx: -1.0, dy: 0.0)) {
|
||||
if abs(rect.topRight.x - previous.maxX) >= innerRadius {
|
||||
var radius = innerRadius
|
||||
if let next = next {
|
||||
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
|
||||
}
|
||||
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topRight.x, y: previous.maxY), type: .topRight, radius: radius)
|
||||
}
|
||||
} else {
|
||||
drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius)
|
||||
}
|
||||
} else {
|
||||
drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius)
|
||||
drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius)
|
||||
}
|
||||
|
||||
if let next = next {
|
||||
if next.contains(rect.bottomLeft) {
|
||||
if abs(rect.bottomRight.x - next.maxX) >= innerRadius {
|
||||
var radius = innerRadius
|
||||
if let previous = previous {
|
||||
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
|
||||
}
|
||||
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomLeft.x, y: next.minY), type: .bottomLeft, radius: radius)
|
||||
}
|
||||
} else {
|
||||
drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius)
|
||||
}
|
||||
if next.contains(rect.bottomRight.offsetBy(dx: -1.0, dy: 0.0)) {
|
||||
if abs(rect.bottomRight.x - next.maxX) >= innerRadius {
|
||||
var radius = innerRadius
|
||||
if let previous = previous {
|
||||
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
|
||||
}
|
||||
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomRight.x, y: next.minY), type: .bottomRight, radius: radius)
|
||||
}
|
||||
} else {
|
||||
drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius)
|
||||
}
|
||||
} else {
|
||||
drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius)
|
||||
drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
public enum ChatMessageThreadInfoType {
|
||||
case bubble(incoming: Bool)
|
||||
case standalone
|
||||
}
|
||||
|
||||
public class ChatMessageStarsMediaInfoNode: ASDisplayNode {
|
||||
public class Arguments {
|
||||
public let presentationData: ChatPresentationData
|
||||
public let context: AccountContext
|
||||
public let message: Message
|
||||
public let media: TelegramMediaPaidContent
|
||||
public let constrainedSize: CGSize
|
||||
public let animationCache: AnimationCache?
|
||||
public let animationRenderer: MultiAnimationRenderer?
|
||||
|
||||
public init(
|
||||
presentationData: ChatPresentationData,
|
||||
context: AccountContext,
|
||||
message: Message,
|
||||
media: TelegramMediaPaidContent,
|
||||
constrainedSize: CGSize,
|
||||
animationCache: AnimationCache?,
|
||||
animationRenderer: MultiAnimationRenderer?
|
||||
) {
|
||||
self.presentationData = presentationData
|
||||
self.context = context
|
||||
self.message = message
|
||||
self.media = media
|
||||
self.constrainedSize = constrainedSize
|
||||
self.animationCache = animationCache
|
||||
self.animationRenderer = animationRenderer
|
||||
}
|
||||
}
|
||||
|
||||
public var visibility: Bool = false {
|
||||
didSet {
|
||||
if self.visibility != oldValue {
|
||||
self.textNode?.visibilityRect = self.visibility ? CGRect.infinite : nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let contentBackgroundNode: ASImageNode
|
||||
private var textNode: TextNodeWithEntities?
|
||||
|
||||
override public init() {
|
||||
self.contentBackgroundNode = ASImageNode()
|
||||
self.contentBackgroundNode.displaysAsynchronously = false
|
||||
self.contentBackgroundNode.displayWithoutProcessing = true
|
||||
self.contentBackgroundNode.isLayerBacked = true
|
||||
self.contentBackgroundNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.contentBackgroundNode)
|
||||
}
|
||||
|
||||
public class func asyncLayout(_ maybeNode: ChatMessageStarsMediaInfoNode?) -> (_ arguments: Arguments) -> (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode) {
|
||||
let textNodeLayout = TextNodeWithEntities.asyncLayout(maybeNode?.textNode)
|
||||
|
||||
return { arguments in
|
||||
let fontSize = floor(arguments.presentationData.fontSize.baseDisplaySize * 11.0 / 17.0)
|
||||
let textFont = Font.regular(fontSize)
|
||||
|
||||
let text: NSMutableAttributedString
|
||||
if let peer = arguments.message.peers[arguments.message.id.peerId] as? TelegramChannel, peer.flags.contains(.isCreator) || peer.adminRights != nil {
|
||||
text = NSMutableAttributedString(string: "⭐️\(arguments.media.amount)", font: textFont, textColor: .white)
|
||||
} else {
|
||||
text = NSMutableAttributedString(string: "Purchased", font: textFont, textColor: .white)
|
||||
}
|
||||
if let range = text.string.range(of: "⭐️") {
|
||||
text.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: NSRange(range, in: text.string))
|
||||
text.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: text.string))
|
||||
}
|
||||
|
||||
let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: arguments.constrainedSize.width, height: arguments.constrainedSize.height), alignment: .natural, cutout: nil, insets: .zero))
|
||||
|
||||
let padding: CGFloat = 6.0
|
||||
let size = CGSize(width: textLayout.size.width + padding * 2.0, height: 18.0)
|
||||
|
||||
return (size, { attemptSynchronous in
|
||||
let node: ChatMessageStarsMediaInfoNode
|
||||
if let maybeNode = maybeNode {
|
||||
node = maybeNode
|
||||
} else {
|
||||
node = ChatMessageStarsMediaInfoNode()
|
||||
}
|
||||
|
||||
if node.contentBackgroundNode.image == nil {
|
||||
node.contentBackgroundNode.image = generateStretchableFilledCircleImage(radius: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.3))
|
||||
}
|
||||
|
||||
node.textNode?.textNode.displaysAsynchronously = !arguments.presentationData.isPreview
|
||||
|
||||
var textArguments: TextNodeWithEntities.Arguments?
|
||||
if let cache = arguments.animationCache, let renderer = arguments.animationRenderer {
|
||||
textArguments = TextNodeWithEntities.Arguments(context: arguments.context, cache: cache, renderer: renderer, placeholderColor: .clear, attemptSynchronous: attemptSynchronous)
|
||||
}
|
||||
let textNode = textApply(textArguments)
|
||||
textNode.visibilityRect = node.visibility ? CGRect.infinite : nil
|
||||
|
||||
if node.textNode == nil {
|
||||
textNode.textNode.isUserInteractionEnabled = false
|
||||
node.textNode = textNode
|
||||
node.addSubnode(textNode.textNode)
|
||||
}
|
||||
|
||||
node.contentBackgroundNode.frame = CGRect(origin: .zero, size: size)
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: padding, y: floorToScreenPixels((size.height - textLayout.size.height) / 2.0) + UIScreenPixel), size: textLayout.size)
|
||||
textNode.textNode.frame = textFrame
|
||||
|
||||
return node
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatMessageUnlockMediaNode",
|
||||
module_name = "ChatMessageUnlockMediaNode",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/Display",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/LocalizedPeerData",
|
||||
"//submodules/PhotoResources",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/InvisibleInkDustNode",
|
||||
"//submodules/TelegramUI/Components/TextNodeWithEntities",
|
||||
"//submodules/TelegramUI/Components/AnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
||||
"//submodules/WallpaperBackgroundNode",
|
||||
"//submodules/TelegramUI/Components/ChatControllerInteraction",
|
||||
"//submodules/AvatarNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import Display
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import LocalizedPeerData
|
||||
import PhotoResources
|
||||
import TelegramStringFormatting
|
||||
import TextFormat
|
||||
import TextNodeWithEntities
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import ComponentFlow
|
||||
import ChatControllerInteraction
|
||||
|
||||
public class ChatMessageUnlockMediaNode: ASDisplayNode {
|
||||
public class Arguments {
|
||||
public let presentationData: ChatPresentationData
|
||||
public let strings: PresentationStrings
|
||||
public let context: AccountContext
|
||||
public let controllerInteraction: ChatControllerInteraction
|
||||
public let message: Message
|
||||
public let media: TelegramMediaPaidContent
|
||||
public let constrainedSize: CGSize
|
||||
public let animationCache: AnimationCache?
|
||||
public let animationRenderer: MultiAnimationRenderer?
|
||||
|
||||
public init(
|
||||
presentationData: ChatPresentationData,
|
||||
strings: PresentationStrings,
|
||||
context: AccountContext,
|
||||
controllerInteraction: ChatControllerInteraction,
|
||||
message: Message,
|
||||
media: TelegramMediaPaidContent,
|
||||
constrainedSize: CGSize,
|
||||
animationCache: AnimationCache?,
|
||||
animationRenderer: MultiAnimationRenderer?
|
||||
) {
|
||||
self.presentationData = presentationData
|
||||
self.strings = strings
|
||||
self.context = context
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.message = message
|
||||
self.media = media
|
||||
self.constrainedSize = constrainedSize
|
||||
self.animationCache = animationCache
|
||||
self.animationRenderer = animationRenderer
|
||||
}
|
||||
}
|
||||
|
||||
public var visibility: Bool = false {
|
||||
didSet {
|
||||
if self.visibility != oldValue {
|
||||
self.textNode?.visibilityRect = self.visibility ? CGRect.infinite : nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let contentNode: HighlightTrackingButtonNode
|
||||
private let backgroundNode: NavigationBackgroundNode
|
||||
private var textNode: TextNodeWithEntities?
|
||||
|
||||
private var pressed = { }
|
||||
|
||||
private var absolutePosition: (CGRect, CGSize)?
|
||||
|
||||
override public init() {
|
||||
self.contentNode = HighlightTrackingButtonNode()
|
||||
|
||||
self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x000000, alpha: 0.3))
|
||||
|
||||
super.init()
|
||||
|
||||
self.contentNode.isUserInteractionEnabled = true
|
||||
|
||||
self.addSubnode(self.contentNode)
|
||||
self.contentNode.addSubnode(self.backgroundNode)
|
||||
|
||||
// self.contentNode.highligthedChanged = { [weak self] highlighted in
|
||||
// if let strongSelf = self {
|
||||
// if highlighted, !strongSelf.frame.width.isZero {
|
||||
// let scale = (strongSelf.frame.width - 10.0) / strongSelf.frame.width
|
||||
//
|
||||
// strongSelf.contentNode.layer.animateScale(from: 1.0, to: scale, duration: 0.15, removeOnCompletion: false)
|
||||
//
|
||||
// strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity")
|
||||
// strongSelf.backgroundNode.alpha = 0.2
|
||||
// } else if let presentationLayer = strongSelf.contentNode.layer.presentation() {
|
||||
// strongSelf.contentNode.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false)
|
||||
//
|
||||
// strongSelf.backgroundNode.alpha = 1.0
|
||||
// strongSelf.backgroundNode.layer.animateAlpha(from: 0.2, to: 1.0, duration: 0.2)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
self.contentNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
self.pressed()
|
||||
}
|
||||
|
||||
public class func asyncLayout(_ maybeNode: ChatMessageUnlockMediaNode?) -> (_ arguments: Arguments) -> (CGSize, (Bool) -> ChatMessageUnlockMediaNode) {
|
||||
let textNodeLayout = TextNodeWithEntities.asyncLayout(maybeNode?.textNode)
|
||||
|
||||
return { arguments in
|
||||
let fontSize = floor(arguments.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)
|
||||
let textFont = Font.medium(fontSize)
|
||||
|
||||
let padding: CGFloat = 10.0
|
||||
//TODO:localize
|
||||
let text = NSMutableAttributedString(string: "Unlock for ⭐️ \(arguments.media.amount)", font: textFont, textColor: .white)
|
||||
if let range = text.string.range(of: "⭐️") {
|
||||
text.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: text.string))
|
||||
text.addAttribute(.baselineOffset, value: 0.5, range: NSRange(range, in: text.string))
|
||||
}
|
||||
|
||||
let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: arguments.constrainedSize.width, height: arguments.constrainedSize.height), alignment: .natural, cutout: nil, insets: .zero))
|
||||
|
||||
let size = CGSize(width: textLayout.size.width + padding * 2.0, height: 32.0)
|
||||
|
||||
return (size, { attemptSynchronous in
|
||||
let node: ChatMessageUnlockMediaNode
|
||||
if let maybeNode = maybeNode {
|
||||
node = maybeNode
|
||||
} else {
|
||||
node = ChatMessageUnlockMediaNode()
|
||||
}
|
||||
|
||||
node.pressed = {
|
||||
let _ = arguments.controllerInteraction.openMessage(arguments.message, OpenMessageParams(mode: .default))
|
||||
}
|
||||
|
||||
node.textNode?.textNode.displaysAsynchronously = !arguments.presentationData.isPreview
|
||||
|
||||
var textArguments: TextNodeWithEntities.Arguments?
|
||||
if let cache = arguments.animationCache, let renderer = arguments.animationRenderer {
|
||||
textArguments = TextNodeWithEntities.Arguments(context: arguments.context, cache: cache, renderer: renderer, placeholderColor: .clear, attemptSynchronous: attemptSynchronous)
|
||||
}
|
||||
let textNode = textApply(textArguments)
|
||||
textNode.visibilityRect = node.visibility ? CGRect.infinite : nil
|
||||
|
||||
if node.textNode == nil {
|
||||
textNode.textNode.isUserInteractionEnabled = false
|
||||
node.textNode = textNode
|
||||
node.contentNode.addSubnode(textNode.textNode)
|
||||
}
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: padding, y: floorToScreenPixels((size.height - textLayout.size.height) / 2.0)), size: textLayout.size)
|
||||
textNode.textNode.frame = textFrame
|
||||
|
||||
node.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
node.backgroundNode.update(size: size, cornerRadius: size.height / 2.0, transition: .immediate)
|
||||
node.contentNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
return node
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -230,7 +230,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||
break
|
||||
}
|
||||
}
|
||||
let gallerySource = GalleryControllerItemSource.standaloneMessage(message)
|
||||
let gallerySource = GalleryControllerItemSource.standaloneMessage(message, nil)
|
||||
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, chatLocation: nil, chatFilterTag: nil, chatLocationContextHolder: nil, message: message, standalone: true, reverseMessageGalleryOrder: false, navigationController: navigationController, dismissInput: {
|
||||
//self?.chatDisplayNode.dismissInput()
|
||||
}, present: { c, a in
|
||||
|
|
|
|||
|
|
@ -109,10 +109,12 @@ public struct NavigateToMessageParams {
|
|||
|
||||
public struct OpenMessageParams {
|
||||
public var mode: ChatControllerInteractionOpenMessageMode
|
||||
public var mediaIndex: Int?
|
||||
public var progress: Promise<Bool>?
|
||||
|
||||
public init(mode: ChatControllerInteractionOpenMessageMode, progress: Promise<Bool>? = nil) {
|
||||
public init(mode: ChatControllerInteractionOpenMessageMode, mediaIndex: Int? = nil, progress: Promise<Bool>? = nil) {
|
||||
self.mode = mode
|
||||
self.mediaIndex = mediaIndex
|
||||
self.progress = progress
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2129,7 +2129,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||
|
||||
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
|
||||
|
||||
let gallery = GalleryController(context: strongSelf.context, source: .standaloneMessage(message), streamSingleVideo: true, replaceRootController: { _, _ in
|
||||
let gallery = GalleryController(context: strongSelf.context, source: .standaloneMessage(message, nil), streamSingleVideo: true, replaceRootController: { _, _ in
|
||||
}, baseNavigationController: nil)
|
||||
gallery.setHintWillBePresentedInPreviewingContext(true)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "StarsAvatarComponent",
|
||||
module_name = "StarsAvatarComponent",
|
||||
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/TelegramPresentationData",
|
||||
"//submodules/PhotoResources",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,472 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
import PhotoResources
|
||||
import AvatarNode
|
||||
import AccountContext
|
||||
import BundleIconComponent
|
||||
import MultilineTextComponent
|
||||
|
||||
public final class StarsAvatarComponent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let peer: StarsContext.State.Transaction.Peer
|
||||
let photo: TelegramMediaWebFile?
|
||||
let media: [Media]
|
||||
|
||||
public init(context: AccountContext, theme: PresentationTheme, peer: StarsContext.State.Transaction.Peer, photo: TelegramMediaWebFile?, media: [Media]) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.peer = peer
|
||||
self.photo = photo
|
||||
self.media = media
|
||||
}
|
||||
|
||||
public static func ==(lhs: StarsAvatarComponent, rhs: StarsAvatarComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.photo != rhs.photo {
|
||||
return false
|
||||
}
|
||||
if !areMediaArraysEqual(lhs.media, rhs.media) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let avatarNode: AvatarNode
|
||||
private let backgroundView = UIImageView()
|
||||
private let iconView = UIImageView()
|
||||
private var imageNode: TransformImageNode?
|
||||
|
||||
private let fetchDisposable = MetaDisposable()
|
||||
|
||||
private var component: StarsAvatarComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0))
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.iconView.contentMode = .scaleAspectFit
|
||||
|
||||
self.addSubnode(self.avatarNode)
|
||||
self.addSubview(self.backgroundView)
|
||||
self.addSubview(self.iconView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.fetchDisposable.dispose()
|
||||
}
|
||||
|
||||
func update(component: StarsAvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let size = CGSize(width: 40.0, height: 40.0)
|
||||
var iconInset: CGFloat = 3.0
|
||||
var iconOffset: CGFloat = 0.0
|
||||
|
||||
var dimensions = size
|
||||
|
||||
switch component.peer {
|
||||
case let .peer(peer):
|
||||
if !component.media.isEmpty {
|
||||
let imageNode: TransformImageNode
|
||||
if let current = self.imageNode {
|
||||
imageNode = current
|
||||
} else {
|
||||
imageNode = TransformImageNode()
|
||||
imageNode.contentAnimations = [.subsequentUpdates]
|
||||
self.addSubview(imageNode.view)
|
||||
self.imageNode = imageNode
|
||||
|
||||
if let image = component.media.first as? TelegramMediaImage {
|
||||
if let imageDimensions = largestImageRepresentation(image.representations)?.dimensions {
|
||||
dimensions = imageDimensions.cgSize.aspectFilled(size)
|
||||
}
|
||||
imageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false))
|
||||
self.fetchDisposable.set(chatMessagePhotoInteractiveFetched(context: component.context, userLocation: .other, photoReference: .standalone(media: image), displayAtSize: nil, storeToDownloadsPeerId: nil).startStrict())
|
||||
} else if let file = component.media.first as? TelegramMediaFile {
|
||||
if let videoDimensions = file.dimensions {
|
||||
dimensions = videoDimensions.cgSize.aspectFilled(size)
|
||||
}
|
||||
imageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file), autoFetchFullSizeThumbnail: true))
|
||||
}
|
||||
}
|
||||
|
||||
imageNode.frame = CGRect(origin: .zero, size: size)
|
||||
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 8.0), imageSize: dimensions, boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
|
||||
|
||||
self.backgroundView.isHidden = true
|
||||
self.iconView.isHidden = true
|
||||
self.avatarNode.isHidden = true
|
||||
} else if let photo = component.photo {
|
||||
let imageNode: TransformImageNode
|
||||
if let current = self.imageNode {
|
||||
imageNode = current
|
||||
} else {
|
||||
imageNode = TransformImageNode()
|
||||
imageNode.contentAnimations = [.subsequentUpdates]
|
||||
self.addSubview(imageNode.view)
|
||||
self.imageNode = imageNode
|
||||
|
||||
imageNode.setSignal(chatWebFileImage(account: component.context.account, file: photo))
|
||||
self.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: component.context.account, userLocation: .other, image: photo).startStrict())
|
||||
}
|
||||
|
||||
imageNode.frame = CGRect(origin: .zero, size: size)
|
||||
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 8.0), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
|
||||
|
||||
self.backgroundView.isHidden = true
|
||||
self.iconView.isHidden = true
|
||||
self.avatarNode.isHidden = true
|
||||
} else {
|
||||
self.avatarNode.setPeer(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
peer: peer,
|
||||
synchronousLoad: true
|
||||
)
|
||||
self.backgroundView.isHidden = true
|
||||
self.iconView.isHidden = true
|
||||
self.avatarNode.isHidden = false
|
||||
}
|
||||
case .appStore:
|
||||
self.backgroundView.image = generateGradientFilledCircleImage(
|
||||
diameter: size.width,
|
||||
colors: [
|
||||
UIColor(rgb: 0x2a9ef1).cgColor,
|
||||
UIColor(rgb: 0x72d5fd).cgColor
|
||||
],
|
||||
direction: .mirroredDiagonal
|
||||
)
|
||||
self.backgroundView.isHidden = false
|
||||
self.iconView.isHidden = false
|
||||
self.avatarNode.isHidden = true
|
||||
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Apple")
|
||||
case .playMarket:
|
||||
self.backgroundView.image = generateGradientFilledCircleImage(
|
||||
diameter: size.width,
|
||||
colors: [
|
||||
UIColor(rgb: 0x54cb68).cgColor,
|
||||
UIColor(rgb: 0xa0de7e).cgColor
|
||||
],
|
||||
direction: .mirroredDiagonal
|
||||
)
|
||||
self.backgroundView.isHidden = false
|
||||
self.iconView.isHidden = false
|
||||
self.avatarNode.isHidden = true
|
||||
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Google")
|
||||
case .fragment:
|
||||
self.backgroundView.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1b1f24))
|
||||
self.backgroundView.isHidden = false
|
||||
self.iconView.isHidden = false
|
||||
self.avatarNode.isHidden = true
|
||||
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment")
|
||||
iconOffset = 2.0
|
||||
case .premiumBot:
|
||||
iconInset = 7.0
|
||||
self.backgroundView.image = generateGradientFilledCircleImage(
|
||||
diameter: size.width,
|
||||
colors: [
|
||||
UIColor(rgb: 0x6b93ff).cgColor,
|
||||
UIColor(rgb: 0x6b93ff).cgColor,
|
||||
UIColor(rgb: 0x8d77ff).cgColor,
|
||||
UIColor(rgb: 0xb56eec).cgColor,
|
||||
UIColor(rgb: 0xb56eec).cgColor
|
||||
],
|
||||
direction: .mirroredDiagonal
|
||||
)
|
||||
self.backgroundView.isHidden = false
|
||||
self.iconView.isHidden = false
|
||||
self.avatarNode.isHidden = true
|
||||
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white)
|
||||
case .unsupported:
|
||||
iconInset = 7.0
|
||||
self.backgroundView.image = generateGradientFilledCircleImage(
|
||||
diameter: size.width,
|
||||
colors: [
|
||||
UIColor(rgb: 0xb1b1b1).cgColor,
|
||||
UIColor(rgb: 0xcdcdcd).cgColor
|
||||
],
|
||||
direction: .mirroredDiagonal
|
||||
)
|
||||
self.backgroundView.isHidden = false
|
||||
self.iconView.isHidden = false
|
||||
self.avatarNode.isHidden = true
|
||||
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white)
|
||||
}
|
||||
|
||||
self.avatarNode.frame = CGRect(origin: .zero, size: size)
|
||||
self.iconView.frame = CGRect(origin: .zero, size: size).insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset)
|
||||
self.backgroundView.frame = CGRect(origin: .zero, size: size)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
public final class StarsLabelComponent: CombinedComponent {
|
||||
let text: NSAttributedString
|
||||
|
||||
public init(
|
||||
text: NSAttributedString
|
||||
) {
|
||||
self.text = text
|
||||
}
|
||||
|
||||
public static func ==(lhs: StarsLabelComponent, rhs: StarsLabelComponent) -> Bool {
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public static var body: Body {
|
||||
let text = Child(MultilineTextComponent.self)
|
||||
let icon = Child(BundleIconComponent.self)
|
||||
|
||||
return { context in
|
||||
let component = context.component
|
||||
|
||||
let text = text.update(
|
||||
component: MultilineTextComponent(text: .plain(component.text)),
|
||||
availableSize: CGSize(width: 100.0, height: 40.0),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let iconSize = CGSize(width: 20.0, height: 20.0)
|
||||
let icon = icon.update(
|
||||
component: BundleIconComponent(
|
||||
name: "Premium/Stars/StarLarge",
|
||||
tintColor: nil
|
||||
),
|
||||
availableSize: iconSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let spacing: CGFloat = 3.0
|
||||
let totalWidth = text.size.width + spacing + iconSize.width
|
||||
let size = CGSize(width: totalWidth, height: iconSize.height)
|
||||
|
||||
context.add(text
|
||||
.position(CGPoint(x: text.size.width / 2.0, y: size.height / 2.0))
|
||||
)
|
||||
context.add(icon
|
||||
.position(CGPoint(x: totalWidth - iconSize.width / 2.0, y: size.height / 2.0 - UIScreenPixel))
|
||||
)
|
||||
return size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class StarsMediaComponent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let peer: StarsContext.State.Transaction.Peer
|
||||
let photo: TelegramMediaWebFile?
|
||||
|
||||
public init(context: AccountContext, theme: PresentationTheme, peer: StarsContext.State.Transaction.Peer, photo: TelegramMediaWebFile?) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.peer = peer
|
||||
self.photo = photo
|
||||
}
|
||||
|
||||
public static func ==(lhs: StarsMediaComponent, rhs: StarsMediaComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.photo != rhs.photo {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let avatarNode: AvatarNode
|
||||
private let backgroundView = UIImageView()
|
||||
private let iconView = UIImageView()
|
||||
private var imageNode: TransformImageNode?
|
||||
|
||||
private let fetchDisposable = MetaDisposable()
|
||||
|
||||
private var component: StarsMediaComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0))
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.iconView.contentMode = .scaleAspectFit
|
||||
|
||||
self.addSubnode(self.avatarNode)
|
||||
self.addSubview(self.backgroundView)
|
||||
self.addSubview(self.iconView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.fetchDisposable.dispose()
|
||||
}
|
||||
|
||||
func update(component: StarsMediaComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let size = CGSize(width: 40.0, height: 40.0)
|
||||
var iconInset: CGFloat = 3.0
|
||||
var iconOffset: CGFloat = 0.0
|
||||
|
||||
switch component.peer {
|
||||
case let .peer(peer):
|
||||
if let photo = component.photo {
|
||||
let imageNode: TransformImageNode
|
||||
if let current = self.imageNode {
|
||||
imageNode = current
|
||||
} else {
|
||||
imageNode = TransformImageNode()
|
||||
imageNode.contentAnimations = [.subsequentUpdates]
|
||||
self.addSubview(imageNode.view)
|
||||
self.imageNode = imageNode
|
||||
|
||||
imageNode.setSignal(chatWebFileImage(account: component.context.account, file: photo))
|
||||
self.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: component.context.account, userLocation: .other, image: photo).startStrict())
|
||||
}
|
||||
|
||||
imageNode.frame = CGRect(origin: .zero, size: size)
|
||||
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: size.width / 2.0), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
|
||||
|
||||
self.backgroundView.isHidden = true
|
||||
self.iconView.isHidden = true
|
||||
self.avatarNode.isHidden = true
|
||||
} else {
|
||||
self.avatarNode.setPeer(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
peer: peer,
|
||||
synchronousLoad: true
|
||||
)
|
||||
self.backgroundView.isHidden = true
|
||||
self.iconView.isHidden = true
|
||||
self.avatarNode.isHidden = false
|
||||
}
|
||||
case .appStore:
|
||||
self.backgroundView.image = generateGradientFilledCircleImage(
|
||||
diameter: size.width,
|
||||
colors: [
|
||||
UIColor(rgb: 0x2a9ef1).cgColor,
|
||||
UIColor(rgb: 0x72d5fd).cgColor
|
||||
],
|
||||
direction: .mirroredDiagonal
|
||||
)
|
||||
self.backgroundView.isHidden = false
|
||||
self.iconView.isHidden = false
|
||||
self.avatarNode.isHidden = true
|
||||
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Apple")
|
||||
case .playMarket:
|
||||
self.backgroundView.image = generateGradientFilledCircleImage(
|
||||
diameter: size.width,
|
||||
colors: [
|
||||
UIColor(rgb: 0x54cb68).cgColor,
|
||||
UIColor(rgb: 0xa0de7e).cgColor
|
||||
],
|
||||
direction: .mirroredDiagonal
|
||||
)
|
||||
self.backgroundView.isHidden = false
|
||||
self.iconView.isHidden = false
|
||||
self.avatarNode.isHidden = true
|
||||
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Google")
|
||||
case .fragment:
|
||||
self.backgroundView.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1b1f24))
|
||||
self.backgroundView.isHidden = false
|
||||
self.iconView.isHidden = false
|
||||
self.avatarNode.isHidden = true
|
||||
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment")
|
||||
iconOffset = 2.0
|
||||
case .premiumBot:
|
||||
iconInset = 7.0
|
||||
self.backgroundView.image = generateGradientFilledCircleImage(
|
||||
diameter: size.width,
|
||||
colors: [
|
||||
UIColor(rgb: 0x6b93ff).cgColor,
|
||||
UIColor(rgb: 0x6b93ff).cgColor,
|
||||
UIColor(rgb: 0x8d77ff).cgColor,
|
||||
UIColor(rgb: 0xb56eec).cgColor,
|
||||
UIColor(rgb: 0xb56eec).cgColor
|
||||
],
|
||||
direction: .mirroredDiagonal
|
||||
)
|
||||
self.backgroundView.isHidden = false
|
||||
self.iconView.isHidden = false
|
||||
self.avatarNode.isHidden = true
|
||||
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white)
|
||||
case .unsupported:
|
||||
iconInset = 7.0
|
||||
self.backgroundView.image = generateGradientFilledCircleImage(
|
||||
diameter: size.width,
|
||||
colors: [
|
||||
UIColor(rgb: 0xb1b1b1).cgColor,
|
||||
UIColor(rgb: 0xcdcdcd).cgColor
|
||||
],
|
||||
direction: .mirroredDiagonal
|
||||
)
|
||||
self.backgroundView.isHidden = false
|
||||
self.iconView.isHidden = false
|
||||
self.avatarNode.isHidden = true
|
||||
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white)
|
||||
}
|
||||
|
||||
self.avatarNode.frame = CGRect(origin: .zero, size: size)
|
||||
self.iconView.frame = CGRect(origin: .zero, size: size).insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset)
|
||||
self.backgroundView.frame = CGRect(origin: .zero, size: size)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -247,25 +247,64 @@ public final class StarsImageComponent: Component {
|
|||
public enum Subject: Equatable {
|
||||
case none
|
||||
case photo(TelegramMediaWebFile)
|
||||
case extendedMedia(TelegramExtendedMedia)
|
||||
case media([Media])
|
||||
case extendedMedia([TelegramExtendedMedia])
|
||||
case transactionPeer(StarsContext.State.Transaction.Peer)
|
||||
|
||||
public static func == (lhs: StarsImageComponent.Subject, rhs: StarsImageComponent.Subject) -> Bool {
|
||||
switch lhs {
|
||||
case .none:
|
||||
if case .none = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .photo(lhsPhoto):
|
||||
if case let .photo(rhsPhoto) = rhs, lhsPhoto == rhsPhoto {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .media(lhsMedia):
|
||||
if case let .media(rhsMedia) = rhs, areMediaArraysEqual(lhsMedia, rhsMedia) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .extendedMedia(lhsExtendedMedia):
|
||||
if case let .extendedMedia(rhsExtendedMedia) = rhs, lhsExtendedMedia == rhsExtendedMedia {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .transactionPeer(lhsPeer):
|
||||
if case let .transactionPeer(rhsPeer) = rhs, lhsPeer == rhsPeer {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public let context: AccountContext
|
||||
public let subject: Subject
|
||||
public let theme: PresentationTheme
|
||||
public let diameter: CGFloat
|
||||
public let backgroundColor: UIColor
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
subject: Subject,
|
||||
theme: PresentationTheme,
|
||||
diameter: CGFloat
|
||||
diameter: CGFloat,
|
||||
backgroundColor: UIColor
|
||||
) {
|
||||
self.context = context
|
||||
self.subject = subject
|
||||
self.theme = theme
|
||||
self.diameter = diameter
|
||||
self.backgroundColor = backgroundColor
|
||||
}
|
||||
|
||||
public static func ==(lhs: StarsImageComponent, rhs: StarsImageComponent) -> Bool {
|
||||
|
|
@ -275,9 +314,15 @@ public final class StarsImageComponent: Component {
|
|||
if lhs.subject != rhs.subject {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.diameter != rhs.diameter {
|
||||
return false
|
||||
}
|
||||
if lhs.backgroundColor != rhs.backgroundColor {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -288,11 +333,15 @@ public final class StarsImageComponent: Component {
|
|||
private var largeParticlesView: StarsParticlesView?
|
||||
|
||||
private var imageNode: TransformImageNode?
|
||||
private var imageFrameNode: UIView?
|
||||
private var secondImageNode: TransformImageNode?
|
||||
private var avatarNode: ImageNode?
|
||||
private var iconBackgroundView: UIImageView?
|
||||
private var iconView: UIImageView?
|
||||
private var dustNode: MediaDustNode?
|
||||
|
||||
private var countView = ComponentView<Empty>()
|
||||
|
||||
private let fetchDisposable = MetaDisposable()
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
|
|
@ -355,6 +404,77 @@ public final class StarsImageComponent: Component {
|
|||
|
||||
imageNode.frame = imageFrame
|
||||
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: imageSize.width / 2.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
|
||||
case let .media(media):
|
||||
let imageNode: TransformImageNode
|
||||
var dimensions = imageSize
|
||||
if let current = self.imageNode {
|
||||
imageNode = current
|
||||
} else {
|
||||
imageNode = TransformImageNode()
|
||||
imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
|
||||
self.addSubview(imageNode.view)
|
||||
self.imageNode = imageNode
|
||||
|
||||
if let image = media.first as? TelegramMediaImage {
|
||||
if let imageDimensions = largestImageRepresentation(image.representations)?.dimensions {
|
||||
dimensions = imageDimensions.cgSize.aspectFilled(imageSize)
|
||||
}
|
||||
imageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false))
|
||||
} else if let file = media.first as? TelegramMediaFile {
|
||||
if let videoDimensions = file.dimensions {
|
||||
dimensions = videoDimensions.cgSize.aspectFilled(imageSize)
|
||||
}
|
||||
imageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file), useLargeThumbnail: true, autoFetchFullSizeThumbnail: true))
|
||||
}
|
||||
}
|
||||
imageNode.frame = imageFrame
|
||||
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: dimensions, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
|
||||
|
||||
if media.count > 1 {
|
||||
let secondImageNode: TransformImageNode
|
||||
let imageFrameNode: UIView
|
||||
if let current = self.secondImageNode, let currentFrame = self.imageFrameNode {
|
||||
secondImageNode = current
|
||||
imageFrameNode = currentFrame
|
||||
} else {
|
||||
secondImageNode = TransformImageNode()
|
||||
secondImageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
|
||||
self.insertSubview(secondImageNode.view, belowSubview: imageNode.view)
|
||||
self.secondImageNode = secondImageNode
|
||||
|
||||
imageFrameNode = UIView()
|
||||
imageFrameNode.layer.cornerRadius = 17.0
|
||||
self.insertSubview(imageFrameNode, belowSubview: imageNode.view)
|
||||
self.imageFrameNode = imageFrameNode
|
||||
|
||||
if let image = media[1] as? TelegramMediaImage {
|
||||
if let imageDimensions = largestImageRepresentation(image.representations)?.dimensions {
|
||||
dimensions = imageDimensions.cgSize.aspectFilled(imageSize)
|
||||
}
|
||||
secondImageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false))
|
||||
}
|
||||
}
|
||||
imageFrameNode.backgroundColor = component.backgroundColor
|
||||
secondImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
|
||||
secondImageNode.frame = imageFrame.offsetBy(dx: 6.0, dy: -6.0)
|
||||
imageFrameNode.frame = imageFrame.insetBy(dx: -2.0, dy: -2.0)
|
||||
|
||||
let countSize = self.countView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
Text(text: "\(media.count)", font: Font.with(size: 30.0, design: .round, weight: .medium), color: .white)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: imageFrame.size
|
||||
)
|
||||
let countFrame = CGRect(origin: CGPoint(x: imageFrame.minX + floorToScreenPixels((imageFrame.width - countSize.width) / 2.0), y: imageFrame.minY + floorToScreenPixels((imageFrame.height - countSize.height) / 2.0)), size: countSize)
|
||||
if let countView = self.countView.view {
|
||||
if countView.superview == nil {
|
||||
self.addSubview(countView)
|
||||
}
|
||||
countView.frame = countFrame
|
||||
}
|
||||
}
|
||||
case let .extendedMedia(extendedMedia):
|
||||
let imageNode: TransformImageNode
|
||||
let dustNode: MediaDustNode
|
||||
|
|
@ -368,12 +488,12 @@ public final class StarsImageComponent: Component {
|
|||
self.imageNode = imageNode
|
||||
|
||||
let media: TelegramMediaImage
|
||||
switch extendedMedia {
|
||||
switch extendedMedia.first {
|
||||
case let .preview(_, immediateThumbnailData, _):
|
||||
let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
||||
media = thumbnailMedia
|
||||
case let .full(fullMedia):
|
||||
media = fullMedia as! TelegramMediaImage
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
|
||||
imageNode.setSignal(chatSecretPhoto(account: component.context.account, userLocation: .other, photoReference: .standalone(media: media), ignoreFullSize: true, synchronousLoad: true))
|
||||
|
|
@ -383,11 +503,63 @@ public final class StarsImageComponent: Component {
|
|||
self.dustNode = dustNode
|
||||
}
|
||||
|
||||
if extendedMedia.count > 1 {
|
||||
let secondImageNode: TransformImageNode
|
||||
let imageFrameNode: UIView
|
||||
if let current = self.secondImageNode, let currentFrame = self.imageFrameNode {
|
||||
secondImageNode = current
|
||||
imageFrameNode = currentFrame
|
||||
} else {
|
||||
secondImageNode = TransformImageNode()
|
||||
secondImageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
|
||||
self.insertSubview(secondImageNode.view, belowSubview: imageNode.view)
|
||||
self.secondImageNode = secondImageNode
|
||||
|
||||
imageFrameNode = UIView()
|
||||
imageFrameNode.layer.cornerRadius = 17.0
|
||||
self.insertSubview(imageFrameNode, belowSubview: imageNode.view)
|
||||
self.imageFrameNode = imageFrameNode
|
||||
|
||||
let media: TelegramMediaImage
|
||||
switch extendedMedia[1] {
|
||||
case let .preview(_, immediateThumbnailData, _):
|
||||
let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
||||
media = thumbnailMedia
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
|
||||
secondImageNode.setSignal(chatSecretPhoto(account: component.context.account, userLocation: .other, photoReference: .standalone(media: media), ignoreFullSize: true, synchronousLoad: true))
|
||||
}
|
||||
imageFrameNode.backgroundColor = component.backgroundColor
|
||||
secondImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
|
||||
secondImageNode.frame = imageFrame.offsetBy(dx: 6.0, dy: -6.0)
|
||||
imageFrameNode.frame = imageFrame.insetBy(dx: -2.0, dy: -2.0)
|
||||
}
|
||||
|
||||
imageNode.frame = imageFrame
|
||||
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 12.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
|
||||
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
|
||||
|
||||
dustNode.frame = imageFrame
|
||||
dustNode.update(size: imageFrame.size, color: .white, transition: .immediate)
|
||||
|
||||
if extendedMedia.count > 1 {
|
||||
let countSize = self.countView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
Text(text: "\(extendedMedia.count)", font: Font.with(size: 30.0, design: .round, weight: .medium), color: .white)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: imageFrame.size
|
||||
)
|
||||
let countFrame = CGRect(origin: CGPoint(x: imageFrame.minX + floorToScreenPixels((imageFrame.width - countSize.width) / 2.0), y: imageFrame.minY + floorToScreenPixels((imageFrame.height - countSize.height) / 2.0)), size: countSize)
|
||||
if let countView = self.countView.view {
|
||||
if countView.superview == nil {
|
||||
self.addSubview(countView)
|
||||
}
|
||||
countView.frame = countFrame
|
||||
}
|
||||
}
|
||||
case let .transactionPeer(peer):
|
||||
if case let .peer(peer) = peer {
|
||||
let avatarNode: ImageNode
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "StarsTransactionScreen",
|
||||
module_name = "StarsTransactionScreen",
|
||||
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/TelegramStringFormatting",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/Components/SheetComponent",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/Components/SolidRoundedButtonComponent",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsImageComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -2,6 +2,7 @@ import Foundation
|
|||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
|
|
@ -29,6 +30,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
let action: () -> Void
|
||||
let cancel: (Bool) -> Void
|
||||
let openPeer: (EnginePeer) -> Void
|
||||
let openMessage: (EngineMessage.Id) -> Void
|
||||
let copyTransactionId: () -> Void
|
||||
|
||||
init(
|
||||
|
|
@ -37,6 +39,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
action: @escaping () -> Void,
|
||||
cancel: @escaping (Bool) -> Void,
|
||||
openPeer: @escaping (EnginePeer) -> Void,
|
||||
openMessage: @escaping (EngineMessage.Id) -> Void,
|
||||
copyTransactionId: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
|
|
@ -44,6 +47,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
self.action = action
|
||||
self.cancel = cancel
|
||||
self.openPeer = openPeer
|
||||
self.openMessage = openMessage
|
||||
self.copyTransactionId = copyTransactionId
|
||||
}
|
||||
|
||||
|
|
@ -172,17 +176,23 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
let transactionId: String?
|
||||
let date: Int32
|
||||
let via: String?
|
||||
let messageId: EngineMessage.Id?
|
||||
let toPeer: EnginePeer?
|
||||
let transactionPeer: StarsContext.State.Transaction.Peer?
|
||||
let media: [Media]
|
||||
let photo: TelegramMediaWebFile?
|
||||
let isRefund: Bool
|
||||
|
||||
var delayedCloseOnOpenPeer = true
|
||||
switch subject {
|
||||
case let .transaction(transaction, isAccount):
|
||||
case let .transaction(transaction, parentPeer):
|
||||
switch transaction.peer {
|
||||
case let .peer(peer):
|
||||
titleText = transaction.title ?? peer.compactDisplayTitle
|
||||
if !transaction.media.isEmpty {
|
||||
titleText = strings.Stars_Transaction_MediaPurchase
|
||||
} else {
|
||||
titleText = transaction.title ?? peer.compactDisplayTitle
|
||||
}
|
||||
via = nil
|
||||
case .appStore:
|
||||
titleText = strings.Stars_Transaction_AppleTopUp_Title
|
||||
|
|
@ -194,7 +204,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
titleText = strings.Stars_Transaction_PremiumBotTopUp_Title
|
||||
via = strings.Stars_Transaction_PremiumBotTopUp_Subtitle
|
||||
case .fragment:
|
||||
if isAccount {
|
||||
if parentPeer.id == component.context.account.peerId {
|
||||
titleText = strings.Stars_Transaction_FragmentTopUp_Title
|
||||
via = strings.Stars_Transaction_FragmentTopUp_Subtitle
|
||||
} else {
|
||||
|
|
@ -205,7 +215,49 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
titleText = strings.Stars_Transaction_Unsupported_Title
|
||||
via = nil
|
||||
}
|
||||
descriptionText = transaction.description ?? ""
|
||||
if !transaction.media.isEmpty {
|
||||
var description: String = ""
|
||||
var photoCount: Int = 0
|
||||
var videoCount: Int = 0
|
||||
for media in transaction.media {
|
||||
if let _ = media as? TelegramMediaFile {
|
||||
videoCount += 1
|
||||
} else {
|
||||
photoCount += 1
|
||||
}
|
||||
}
|
||||
//TODO:localize
|
||||
if photoCount > 0 && videoCount > 0 {
|
||||
if photoCount > 1 {
|
||||
description += "\(photoCount) photos**"
|
||||
} else {
|
||||
description += "**\(photoCount) photo**"
|
||||
}
|
||||
description += " and "
|
||||
if videoCount > 1 {
|
||||
description += "**\(videoCount) videos**"
|
||||
} else {
|
||||
description += "**\(videoCount) video**"
|
||||
}
|
||||
} else if photoCount > 0 {
|
||||
if photoCount > 1 {
|
||||
description += "**\(photoCount) photos**"
|
||||
} else {
|
||||
description += "**Photo**"
|
||||
}
|
||||
} else if videoCount > 0 {
|
||||
if videoCount > 1 {
|
||||
description += "**\(videoCount) videos**"
|
||||
} else {
|
||||
description += "**Video**"
|
||||
}
|
||||
}
|
||||
descriptionText = description.replacingOccurrences(of: "**", with: "")
|
||||
} else {
|
||||
descriptionText = transaction.description ?? ""
|
||||
}
|
||||
|
||||
messageId = transaction.paidMessageId
|
||||
|
||||
count = transaction.count
|
||||
transactionId = transaction.id
|
||||
|
|
@ -216,6 +268,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
toPeer = nil
|
||||
}
|
||||
transactionPeer = transaction.peer
|
||||
media = transaction.media
|
||||
photo = transaction.photo
|
||||
isRefund = transaction.flags.contains(.isRefund)
|
||||
case let .receipt(receipt):
|
||||
|
|
@ -223,6 +276,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
descriptionText = receipt.invoiceMedia.description
|
||||
count = (receipt.invoice.prices.first?.amount ?? receipt.invoiceMedia.totalAmount) * -1
|
||||
via = nil
|
||||
messageId = nil
|
||||
transactionId = receipt.transactionId
|
||||
date = receipt.date
|
||||
if let peer = state.peerMap[receipt.botPaymentId] {
|
||||
|
|
@ -231,6 +285,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
toPeer = nil
|
||||
}
|
||||
transactionPeer = nil
|
||||
media = []
|
||||
photo = receipt.invoiceMedia.photo
|
||||
isRefund = false
|
||||
delayedCloseOnOpenPeer = false
|
||||
|
|
@ -261,7 +316,9 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
)
|
||||
|
||||
let imageSubject: StarsImageComponent.Subject
|
||||
if let photo {
|
||||
if !media.isEmpty {
|
||||
imageSubject = .media(media)
|
||||
} else if let photo {
|
||||
imageSubject = .photo(photo)
|
||||
} else if let transactionPeer {
|
||||
imageSubject = .transactionPeer(transactionPeer)
|
||||
|
|
@ -275,7 +332,8 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
context: component.context,
|
||||
subject: imageSubject,
|
||||
theme: theme,
|
||||
diameter: 90.0
|
||||
diameter: 90.0,
|
||||
backgroundColor: theme.actionSheet.opaqueItemBackgroundColor
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: 200.0),
|
||||
transition: .immediate
|
||||
|
|
@ -345,6 +403,41 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
)
|
||||
))
|
||||
}
|
||||
|
||||
if let messageId {
|
||||
//TODO:localize
|
||||
let peerName: String
|
||||
if case let .transaction(_, parentPeer) = component.subject {
|
||||
if parentPeer.id == component.context.account.peerId {
|
||||
if let toPeer {
|
||||
peerName = toPeer.addressName ?? "c/\(toPeer.id.id._internalGetInt64Value())"
|
||||
} else {
|
||||
peerName = ""
|
||||
}
|
||||
} else {
|
||||
peerName = parentPeer.addressName ?? "c/\(parentPeer.id.id._internalGetInt64Value())"
|
||||
}
|
||||
} else {
|
||||
peerName = ""
|
||||
}
|
||||
tableItems.append(.init(
|
||||
id: "via",
|
||||
title: strings.Stars_Transaction_Media,
|
||||
component: AnyComponent(
|
||||
Button(
|
||||
content: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: "t.me/\(peerName)/\(messageId.id)", font: tableFont, textColor: tableLinkColor)))
|
||||
),
|
||||
action: {
|
||||
component.openMessage(messageId)
|
||||
Queue.mainQueue().after(1.0, {
|
||||
component.cancel(false)
|
||||
})
|
||||
}
|
||||
)
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
if let transactionId {
|
||||
tableItems.append(.init(
|
||||
|
|
@ -544,6 +637,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
|
|||
let subject: StarsTransactionScreen.Subject
|
||||
let action: () -> Void
|
||||
let openPeer: (EnginePeer) -> Void
|
||||
let openMessage: (EngineMessage.Id) -> Void
|
||||
let copyTransactionId: () -> Void
|
||||
|
||||
init(
|
||||
|
|
@ -551,12 +645,14 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
|
|||
subject: StarsTransactionScreen.Subject,
|
||||
action: @escaping () -> Void,
|
||||
openPeer: @escaping (EnginePeer) -> Void,
|
||||
openMessage: @escaping (EngineMessage.Id) -> Void,
|
||||
copyTransactionId: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.subject = subject
|
||||
self.action = action
|
||||
self.openPeer = openPeer
|
||||
self.openMessage = openMessage
|
||||
self.copyTransactionId = copyTransactionId
|
||||
}
|
||||
|
||||
|
|
@ -599,6 +695,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
|
|||
}
|
||||
},
|
||||
openPeer: context.component.openPeer,
|
||||
openMessage: context.component.openMessage,
|
||||
copyTransactionId: context.component.copyTransactionId
|
||||
)),
|
||||
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
|
||||
|
|
@ -667,7 +764,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
|
|||
|
||||
public class StarsTransactionScreen: ViewControllerComponentContainer {
|
||||
public enum Subject: Equatable {
|
||||
case transaction(StarsContext.State.Transaction, Bool)
|
||||
case transaction(StarsContext.State.Transaction, EnginePeer)
|
||||
case receipt(BotPaymentReceipt)
|
||||
}
|
||||
|
||||
|
|
@ -685,6 +782,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
|
|||
self.context = context
|
||||
|
||||
var openPeerImpl: ((EnginePeer) -> Void)?
|
||||
var openMessageImpl: ((EngineMessage.Id) -> Void)?
|
||||
var copyTransactionIdImpl: (() -> Void)?
|
||||
super.init(
|
||||
context: context,
|
||||
|
|
@ -695,6 +793,9 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
|
|||
openPeer: { peerId in
|
||||
openPeerImpl?(peerId)
|
||||
},
|
||||
openMessage: { messageId in
|
||||
openMessageImpl?(messageId)
|
||||
},
|
||||
copyTransactionId: {
|
||||
copyTransactionIdImpl?()
|
||||
}
|
||||
|
|
@ -724,6 +825,23 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
|
|||
})
|
||||
}
|
||||
|
||||
openMessageImpl = { [weak self] messageId in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = (context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
guard let peer = peer else {
|
||||
return
|
||||
}
|
||||
if let navigationController = self.navigationController as? NavigationController {
|
||||
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always, useExisting: false, purposefulAction: {}, peekData: nil))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
copyTransactionIdImpl = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
|
|
@ -1210,3 +1328,24 @@ private final class TransactionCellComponent: Component {
|
|||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
|
||||
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setLineWidth(2.0)
|
||||
context.setLineCap(.round)
|
||||
context.setStrokeColor(foregroundColor.cgColor)
|
||||
|
||||
context.move(to: CGPoint(x: 10.0, y: 10.0))
|
||||
context.addLine(to: CGPoint(x: 20.0, y: 20.0))
|
||||
context.strokePath()
|
||||
|
||||
context.move(to: CGPoint(x: 20.0, y: 10.0))
|
||||
context.addLine(to: CGPoint(x: 10.0, y: 20.0))
|
||||
context.strokePath()
|
||||
})
|
||||
}
|
||||
|
|
@ -45,6 +45,8 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController",
|
||||
"//submodules/TelegramUI/Components/ListItemComponentAdaptor",
|
||||
"//submodules/StatisticsUI",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsAvatarComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import UndoUI
|
|||
import ListItemComponentAdaptor
|
||||
import StatisticsUI
|
||||
import ItemListUI
|
||||
import StarsWithdrawalScreen
|
||||
|
||||
final class StarsStatisticsScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
|
@ -644,8 +645,14 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer {
|
|||
guard let self else {
|
||||
return
|
||||
}
|
||||
let controller = context.sharedContext.makeStarsTransactionScreen(context: context, transaction: transaction, isAccount: false)
|
||||
self.push(controller)
|
||||
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self, let peer else {
|
||||
return
|
||||
}
|
||||
let controller = context.sharedContext.makeStarsTransactionScreen(context: context, transaction: transaction, peer: peer)
|
||||
self.push(controller)
|
||||
})
|
||||
}
|
||||
|
||||
withdrawImpl = { [weak self] in
|
||||
|
|
@ -666,7 +673,7 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer {
|
|||
guard let self, let stats = state.stats else {
|
||||
return
|
||||
}
|
||||
let controller = StarsWithdrawScreen(context: context, mode: .withdraw(stats), completion: { [weak self] amount in
|
||||
let controller = self.context.sharedContext.makeStarsWithdrawalScreen(context: context, stats: stats, completion: { [weak self] amount in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import TelegramStringFormatting
|
|||
import AvatarNode
|
||||
import BundleIconComponent
|
||||
import PhotoResources
|
||||
import StarsAvatarComponent
|
||||
|
||||
private extension StarsContext.State.Transaction {
|
||||
var extendedId: String {
|
||||
|
|
@ -207,7 +208,11 @@ final class StarsTransactionsListPanelComponent: Component {
|
|||
var itemDate: String
|
||||
switch item.peer {
|
||||
case let .peer(peer):
|
||||
if let title = item.title {
|
||||
if !item.media.isEmpty {
|
||||
//TODO:localize
|
||||
itemTitle = "Media Purchase"
|
||||
itemSubtitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
|
||||
} else if let title = item.title {
|
||||
itemTitle = title
|
||||
itemSubtitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
|
||||
} else {
|
||||
|
|
@ -291,9 +296,9 @@ final class StarsTransactionsListPanelComponent: Component {
|
|||
theme: environment.theme,
|
||||
title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)),
|
||||
contentInsets: UIEdgeInsets(top: 9.0, left: environment.containerInsets.left, bottom: 8.0, right: environment.containerInsets.right),
|
||||
leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(AvatarComponent(context: component.context, theme: environment.theme, peer: item.peer, photo: item.photo))), false),
|
||||
leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: item.peer, photo: item.photo, media: item.media))), false),
|
||||
icon: nil,
|
||||
accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(LabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))),
|
||||
accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))),
|
||||
action: { [weak self] _ in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
|
|
@ -493,241 +498,3 @@ func cancelContextGestures(view: UIView) {
|
|||
cancelContextGestures(view: subview)
|
||||
}
|
||||
}
|
||||
|
||||
private final class AvatarComponent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let peer: StarsContext.State.Transaction.Peer
|
||||
let photo: TelegramMediaWebFile?
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, peer: StarsContext.State.Transaction.Peer, photo: TelegramMediaWebFile?) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.peer = peer
|
||||
self.photo = photo
|
||||
}
|
||||
|
||||
static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.photo != rhs.photo {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let avatarNode: AvatarNode
|
||||
private let backgroundView = UIImageView()
|
||||
private let iconView = UIImageView()
|
||||
private var imageNode: TransformImageNode?
|
||||
|
||||
private let fetchDisposable = MetaDisposable()
|
||||
|
||||
private var component: AvatarComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0))
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.iconView.contentMode = .scaleAspectFit
|
||||
|
||||
self.addSubnode(self.avatarNode)
|
||||
self.addSubview(self.backgroundView)
|
||||
self.addSubview(self.iconView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.fetchDisposable.dispose()
|
||||
}
|
||||
|
||||
func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let size = CGSize(width: 40.0, height: 40.0)
|
||||
var iconInset: CGFloat = 3.0
|
||||
var iconOffset: CGFloat = 0.0
|
||||
|
||||
switch component.peer {
|
||||
case let .peer(peer):
|
||||
if let photo = component.photo {
|
||||
let imageNode: TransformImageNode
|
||||
if let current = self.imageNode {
|
||||
imageNode = current
|
||||
} else {
|
||||
imageNode = TransformImageNode()
|
||||
imageNode.contentAnimations = [.subsequentUpdates]
|
||||
self.addSubview(imageNode.view)
|
||||
self.imageNode = imageNode
|
||||
|
||||
imageNode.setSignal(chatWebFileImage(account: component.context.account, file: photo))
|
||||
self.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: component.context.account, userLocation: .other, image: photo).startStrict())
|
||||
}
|
||||
|
||||
imageNode.frame = CGRect(origin: .zero, size: size)
|
||||
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: size.width / 2.0), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
|
||||
|
||||
self.backgroundView.isHidden = true
|
||||
self.iconView.isHidden = true
|
||||
self.avatarNode.isHidden = true
|
||||
} else {
|
||||
self.avatarNode.setPeer(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
peer: peer,
|
||||
synchronousLoad: true
|
||||
)
|
||||
self.backgroundView.isHidden = true
|
||||
self.iconView.isHidden = true
|
||||
self.avatarNode.isHidden = false
|
||||
}
|
||||
case .appStore:
|
||||
self.backgroundView.image = generateGradientFilledCircleImage(
|
||||
diameter: size.width,
|
||||
colors: [
|
||||
UIColor(rgb: 0x2a9ef1).cgColor,
|
||||
UIColor(rgb: 0x72d5fd).cgColor
|
||||
],
|
||||
direction: .mirroredDiagonal
|
||||
)
|
||||
self.backgroundView.isHidden = false
|
||||
self.iconView.isHidden = false
|
||||
self.avatarNode.isHidden = true
|
||||
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Apple")
|
||||
case .playMarket:
|
||||
self.backgroundView.image = generateGradientFilledCircleImage(
|
||||
diameter: size.width,
|
||||
colors: [
|
||||
UIColor(rgb: 0x54cb68).cgColor,
|
||||
UIColor(rgb: 0xa0de7e).cgColor
|
||||
],
|
||||
direction: .mirroredDiagonal
|
||||
)
|
||||
self.backgroundView.isHidden = false
|
||||
self.iconView.isHidden = false
|
||||
self.avatarNode.isHidden = true
|
||||
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Google")
|
||||
case .fragment:
|
||||
self.backgroundView.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1b1f24))
|
||||
self.backgroundView.isHidden = false
|
||||
self.iconView.isHidden = false
|
||||
self.avatarNode.isHidden = true
|
||||
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment")
|
||||
iconOffset = 2.0
|
||||
case .premiumBot:
|
||||
iconInset = 7.0
|
||||
self.backgroundView.image = generateGradientFilledCircleImage(
|
||||
diameter: size.width,
|
||||
colors: [
|
||||
UIColor(rgb: 0x6b93ff).cgColor,
|
||||
UIColor(rgb: 0x6b93ff).cgColor,
|
||||
UIColor(rgb: 0x8d77ff).cgColor,
|
||||
UIColor(rgb: 0xb56eec).cgColor,
|
||||
UIColor(rgb: 0xb56eec).cgColor
|
||||
],
|
||||
direction: .mirroredDiagonal
|
||||
)
|
||||
self.backgroundView.isHidden = false
|
||||
self.iconView.isHidden = false
|
||||
self.avatarNode.isHidden = true
|
||||
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white)
|
||||
case .unsupported:
|
||||
iconInset = 7.0
|
||||
self.backgroundView.image = generateGradientFilledCircleImage(
|
||||
diameter: size.width,
|
||||
colors: [
|
||||
UIColor(rgb: 0xb1b1b1).cgColor,
|
||||
UIColor(rgb: 0xcdcdcd).cgColor
|
||||
],
|
||||
direction: .mirroredDiagonal
|
||||
)
|
||||
self.backgroundView.isHidden = false
|
||||
self.iconView.isHidden = false
|
||||
self.avatarNode.isHidden = true
|
||||
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white)
|
||||
}
|
||||
|
||||
self.avatarNode.frame = CGRect(origin: .zero, size: size)
|
||||
self.iconView.frame = CGRect(origin: .zero, size: size).insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset)
|
||||
self.backgroundView.frame = CGRect(origin: .zero, size: size)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private final class LabelComponent: CombinedComponent {
|
||||
let text: NSAttributedString
|
||||
|
||||
init(
|
||||
text: NSAttributedString
|
||||
) {
|
||||
self.text = text
|
||||
}
|
||||
|
||||
static func ==(lhs: LabelComponent, rhs: LabelComponent) -> Bool {
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let text = Child(MultilineTextComponent.self)
|
||||
let icon = Child(BundleIconComponent.self)
|
||||
|
||||
return { context in
|
||||
let component = context.component
|
||||
|
||||
let text = text.update(
|
||||
component: MultilineTextComponent(text: .plain(component.text)),
|
||||
availableSize: CGSize(width: 100.0, height: 40.0),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let iconSize = CGSize(width: 20.0, height: 20.0)
|
||||
let icon = icon.update(
|
||||
component: BundleIconComponent(
|
||||
name: "Premium/Stars/StarLarge",
|
||||
tintColor: nil
|
||||
),
|
||||
availableSize: iconSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let spacing: CGFloat = 3.0
|
||||
let totalWidth = text.size.width + spacing + iconSize.width
|
||||
let size = CGSize(width: totalWidth, height: iconSize.height)
|
||||
|
||||
context.add(text
|
||||
.position(CGPoint(x: text.size.width / 2.0, y: size.height / 2.0))
|
||||
)
|
||||
context.add(icon
|
||||
.position(CGPoint(x: totalWidth - iconSize.width / 2.0, y: size.height / 2.0 - UIScreenPixel))
|
||||
)
|
||||
return size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -723,8 +723,14 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
|
|||
guard let self else {
|
||||
return
|
||||
}
|
||||
let controller = context.sharedContext.makeStarsTransactionScreen(context: context, transaction: transaction, isAccount: true)
|
||||
self.push(controller)
|
||||
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self, let peer else {
|
||||
return
|
||||
}
|
||||
let controller = context.sharedContext.makeStarsTransactionScreen(context: context, transaction: transaction, peer: peer)
|
||||
self.push(controller)
|
||||
})
|
||||
}
|
||||
|
||||
buyImpl = { [weak self] in
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ private final class SheetContent: CombinedComponent {
|
|||
let starsContext: StarsContext
|
||||
let invoice: TelegramMediaInvoice
|
||||
let source: BotPaymentInvoiceSource
|
||||
let extendedMedia: [TelegramExtendedMedia]
|
||||
let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>
|
||||
let dismiss: () -> Void
|
||||
|
||||
|
|
@ -34,6 +35,7 @@ private final class SheetContent: CombinedComponent {
|
|||
starsContext: StarsContext,
|
||||
invoice: TelegramMediaInvoice,
|
||||
source: BotPaymentInvoiceSource,
|
||||
extendedMedia: [TelegramExtendedMedia],
|
||||
inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>,
|
||||
dismiss: @escaping () -> Void
|
||||
) {
|
||||
|
|
@ -41,6 +43,7 @@ private final class SheetContent: CombinedComponent {
|
|||
self.starsContext = starsContext
|
||||
self.invoice = invoice
|
||||
self.source = source
|
||||
self.extendedMedia = extendedMedia
|
||||
self.inputData = inputData
|
||||
self.dismiss = dismiss
|
||||
}
|
||||
|
|
@ -52,6 +55,9 @@ private final class SheetContent: CombinedComponent {
|
|||
if lhs.invoice != rhs.invoice {
|
||||
return false
|
||||
}
|
||||
if lhs.extendedMedia != rhs.extendedMedia {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -64,7 +70,8 @@ private final class SheetContent: CombinedComponent {
|
|||
private let source: BotPaymentInvoiceSource
|
||||
private let invoice: TelegramMediaInvoice
|
||||
|
||||
private(set) var peer: EnginePeer?
|
||||
private(set) var botPeer: EnginePeer?
|
||||
private(set) var chatPeer: EnginePeer?
|
||||
private var peerDisposable: Disposable?
|
||||
private(set) var balance: Int64?
|
||||
private(set) var form: BotPaymentForm?
|
||||
|
|
@ -95,14 +102,25 @@ private final class SheetContent: CombinedComponent {
|
|||
|
||||
super.init()
|
||||
|
||||
self.peerDisposable = (inputData
|
||||
|> deliverOnMainQueue).start(next: { [weak self] inputData in
|
||||
let chatPeer: Signal<EnginePeer?, NoError>
|
||||
if case let .message(messageId) = source {
|
||||
chatPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId))
|
||||
} else {
|
||||
chatPeer = .single(nil)
|
||||
}
|
||||
|
||||
self.peerDisposable = (combineLatest(
|
||||
inputData,
|
||||
chatPeer
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] inputData, chatPeer in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.balance = inputData?.0.balance ?? 0
|
||||
self.form = inputData?.1
|
||||
self.peer = inputData?.2
|
||||
self.botPeer = inputData?.2
|
||||
self.chatPeer = chatPeer
|
||||
self.updated(transition: .immediate)
|
||||
|
||||
if self.optionsDisposable == nil, let balance = self.balance, balance < self.invoice.totalAmount {
|
||||
|
|
@ -228,9 +246,9 @@ private final class SheetContent: CombinedComponent {
|
|||
)
|
||||
|
||||
let subject: StarsImageComponent.Subject
|
||||
if let extendedMedia = component.invoice.extendedMedia {
|
||||
subject = .extendedMedia(extendedMedia)
|
||||
} else if let peer = state.peer {
|
||||
if !component.extendedMedia.isEmpty {
|
||||
subject = .extendedMedia(component.extendedMedia)
|
||||
} else if let peer = state.botPeer {
|
||||
if let photo = component.invoice.photo {
|
||||
subject = .photo(photo)
|
||||
} else {
|
||||
|
|
@ -244,7 +262,8 @@ private final class SheetContent: CombinedComponent {
|
|||
context: component.context,
|
||||
subject: subject,
|
||||
theme: theme,
|
||||
diameter: 90.0
|
||||
diameter: 90.0,
|
||||
backgroundColor: theme.list.blocksBackgroundColor
|
||||
),
|
||||
availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0),
|
||||
transition: context.transition
|
||||
|
|
@ -299,14 +318,52 @@ private final class SheetContent: CombinedComponent {
|
|||
|
||||
let amount = component.invoice.totalAmount
|
||||
let infoText: String
|
||||
if let _ = component.invoice.extendedMedia {
|
||||
if !component.extendedMedia.isEmpty {
|
||||
//TODO:localize
|
||||
var description: String = ""
|
||||
var photoCount: Int = 0
|
||||
var videoCount: Int = 0
|
||||
for media in component.extendedMedia {
|
||||
if case let .preview(_, _, videoDuration) = media, videoDuration != nil {
|
||||
videoCount += 1
|
||||
} else {
|
||||
photoCount += 1
|
||||
}
|
||||
}
|
||||
if photoCount > 0 && videoCount > 0 {
|
||||
if photoCount > 1 {
|
||||
description += "**\(photoCount) photos**"
|
||||
} else {
|
||||
description += "**\(photoCount) photo**"
|
||||
}
|
||||
description += " and "
|
||||
if videoCount > 1 {
|
||||
description += "**\(videoCount) videos**"
|
||||
} else {
|
||||
description += "**\(videoCount) video**"
|
||||
}
|
||||
} else if photoCount > 0 {
|
||||
if photoCount > 1 {
|
||||
description += "**\(photoCount) photos**"
|
||||
} else {
|
||||
description += "**photo**"
|
||||
}
|
||||
} else if videoCount > 0 {
|
||||
if videoCount > 1 {
|
||||
description += "**\(videoCount) videos**"
|
||||
} else {
|
||||
description += "**video**"
|
||||
}
|
||||
}
|
||||
infoText = strings.Stars_Transfer_UnlockInfo(
|
||||
description,
|
||||
state.chatPeer?.compactDisplayTitle ?? "",
|
||||
strings.Stars_Transfer_Info_Stars(Int32(amount))
|
||||
).string
|
||||
} else {
|
||||
infoText = strings.Stars_Transfer_Info(
|
||||
component.invoice.title,
|
||||
state.peer?.compactDisplayTitle ?? "",
|
||||
state.botPeer?.compactDisplayTitle ?? "",
|
||||
strings.Stars_Transfer_Info_Stars(Int32(amount))
|
||||
).string
|
||||
}
|
||||
|
|
@ -386,7 +443,7 @@ private final class SheetContent: CombinedComponent {
|
|||
|
||||
let accountContext = component.context
|
||||
let starsContext = component.starsContext
|
||||
let botTitle = state.peer?.compactDisplayTitle ?? ""
|
||||
let botTitle = state.botPeer?.compactDisplayTitle ?? ""
|
||||
let invoice = component.invoice
|
||||
let button = button.update(
|
||||
component: ButtonComponent(
|
||||
|
|
@ -410,7 +467,7 @@ private final class SheetContent: CombinedComponent {
|
|||
context: accountContext,
|
||||
starsContext: starsContext,
|
||||
options: state?.options ?? [],
|
||||
peerId: state?.peer?.id,
|
||||
peerId: state?.botPeer?.id,
|
||||
requiredStars: invoice.totalAmount,
|
||||
completion: { [weak starsContext] stars in
|
||||
starsContext?.add(balance: stars)
|
||||
|
|
@ -484,6 +541,7 @@ private final class StarsTransferSheetComponent: CombinedComponent {
|
|||
private let starsContext: StarsContext
|
||||
private let invoice: TelegramMediaInvoice
|
||||
private let source: BotPaymentInvoiceSource
|
||||
private let extendedMedia: [TelegramExtendedMedia]
|
||||
private let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>
|
||||
|
||||
init(
|
||||
|
|
@ -491,12 +549,14 @@ private final class StarsTransferSheetComponent: CombinedComponent {
|
|||
starsContext: StarsContext,
|
||||
invoice: TelegramMediaInvoice,
|
||||
source: BotPaymentInvoiceSource,
|
||||
extendedMedia: [TelegramExtendedMedia],
|
||||
inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>
|
||||
) {
|
||||
self.context = context
|
||||
self.starsContext = starsContext
|
||||
self.invoice = invoice
|
||||
self.source = source
|
||||
self.extendedMedia = extendedMedia
|
||||
self.inputData = inputData
|
||||
}
|
||||
|
||||
|
|
@ -507,6 +567,9 @@ private final class StarsTransferSheetComponent: CombinedComponent {
|
|||
if lhs.invoice != rhs.invoice {
|
||||
return false
|
||||
}
|
||||
if lhs.extendedMedia != rhs.extendedMedia {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -526,6 +589,7 @@ private final class StarsTransferSheetComponent: CombinedComponent {
|
|||
starsContext: context.component.starsContext,
|
||||
invoice: context.component.invoice,
|
||||
source: context.component.source,
|
||||
extendedMedia: context.component.extendedMedia,
|
||||
inputData: context.component.inputData,
|
||||
dismiss: {
|
||||
animateOut.invoke(Action { _ in
|
||||
|
|
@ -584,6 +648,7 @@ public final class StarsTransferScreen: ViewControllerComponentContainer {
|
|||
starsContext: StarsContext,
|
||||
invoice: TelegramMediaInvoice,
|
||||
source: BotPaymentInvoiceSource,
|
||||
extendedMedia: [TelegramExtendedMedia],
|
||||
inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>,
|
||||
completion: @escaping (Bool) -> Void
|
||||
) {
|
||||
|
|
@ -597,6 +662,7 @@ public final class StarsTransferScreen: ViewControllerComponentContainer {
|
|||
starsContext: starsContext,
|
||||
invoice: invoice,
|
||||
source: source,
|
||||
extendedMedia: extendedMedia,
|
||||
inputData: inputData
|
||||
),
|
||||
navigationBarAppearance: .none,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "StarsWithdrawalScreen",
|
||||
module_name = "StarsWithdrawalScreen",
|
||||
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/Premium/PremiumStarComponent",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/PasswordSetupUI",
|
||||
"//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -9,7 +9,7 @@ import PasswordSetupUI
|
|||
import Markdown
|
||||
import OwnershipTransferController
|
||||
|
||||
func confirmStarsRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, amount: Int64, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController {
|
||||
public func confirmStarsRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, amount: Int64, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController {
|
||||
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
|
|
@ -73,7 +73,7 @@ func confirmStarsRevenueWithdrawalController(context: AccountContext, updatedPre
|
|||
}
|
||||
|
||||
|
||||
func starsRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, amount: Int64, initialError: RequestStarsRevenueWithdrawalError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController {
|
||||
public func starsRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, amount: Int64, initialError: RequestStarsRevenueWithdrawalError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController {
|
||||
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||
let theme = AlertControllerTheme(presentationData: presentationData)
|
||||
|
||||
|
|
@ -111,13 +111,14 @@ private final class SheetContent: CombinedComponent {
|
|||
let minAmount: Int64?
|
||||
let maxAmount: Int64?
|
||||
|
||||
let configuration = StarsWithdrawConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
|
||||
|
||||
switch component.mode {
|
||||
case let .withdraw(status):
|
||||
titleString = environment.strings.Stars_Withdraw_Title
|
||||
amountTitle = environment.strings.Stars_Withdraw_AmountTitle
|
||||
amountPlaceholder = environment.strings.Stars_Withdraw_AmountPlaceholder
|
||||
|
||||
let configuration = StarsWithdrawConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
|
||||
minAmount = configuration.minWithdrawAmount
|
||||
maxAmount = status.balances.availableBalance
|
||||
case .paidMedia:
|
||||
|
|
@ -126,7 +127,7 @@ private final class SheetContent: CombinedComponent {
|
|||
amountPlaceholder = environment.strings.Stars_PaidContent_AmountPlaceholder
|
||||
|
||||
minAmount = 1
|
||||
maxAmount = nil
|
||||
maxAmount = configuration.maxPaidMediaAmount
|
||||
}
|
||||
|
||||
let title = title.update(
|
||||
|
|
@ -335,8 +336,11 @@ private final class SheetContent: CombinedComponent {
|
|||
|
||||
func makeState() -> State {
|
||||
var amount: Int64?
|
||||
if case let .withdraw(stats) = mode {
|
||||
switch self.mode {
|
||||
case let .withdraw(stats):
|
||||
amount = stats.balances.availableBalance
|
||||
case let .paidMedia(initialValue):
|
||||
amount = initialValue
|
||||
}
|
||||
return State(context: self.context, amount: amount)
|
||||
}
|
||||
|
|
@ -432,7 +436,7 @@ private final class StarsWithdrawSheetComponent: CombinedComponent {
|
|||
public final class StarsWithdrawScreen: ViewControllerComponentContainer {
|
||||
public enum Mode: Equatable {
|
||||
case withdraw(StarsRevenueStats)
|
||||
case paidMedia
|
||||
case paidMedia(Int64?)
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
|
|
@ -730,18 +734,29 @@ func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor
|
|||
|
||||
private struct StarsWithdrawConfiguration {
|
||||
static var defaultValue: StarsWithdrawConfiguration {
|
||||
return StarsWithdrawConfiguration(minWithdrawAmount: nil)
|
||||
return StarsWithdrawConfiguration(minWithdrawAmount: nil, maxPaidMediaAmount: nil)
|
||||
}
|
||||
|
||||
let minWithdrawAmount: Int64?
|
||||
let maxPaidMediaAmount: Int64?
|
||||
|
||||
fileprivate init(minWithdrawAmount: Int64?) {
|
||||
fileprivate init(minWithdrawAmount: Int64?, maxPaidMediaAmount: Int64?) {
|
||||
self.minWithdrawAmount = minWithdrawAmount
|
||||
self.maxPaidMediaAmount = maxPaidMediaAmount
|
||||
}
|
||||
|
||||
static func with(appConfiguration: AppConfiguration) -> StarsWithdrawConfiguration {
|
||||
if let data = appConfiguration.data, let minWithdrawAmount = data["stars_revenue_withdrawal_min"] as? Double {
|
||||
return StarsWithdrawConfiguration(minWithdrawAmount: Int64(minWithdrawAmount))
|
||||
if let data = appConfiguration.data {
|
||||
var minWithdrawAmount: Int64?
|
||||
if let value = data["stars_revenue_withdrawal_min"] as? Double {
|
||||
minWithdrawAmount = Int64(value)
|
||||
}
|
||||
var maxPaidMediaAmount: Int64?
|
||||
if let value = data["stars_paid_post_amount_max"] as? Double {
|
||||
maxPaidMediaAmount = Int64(value)
|
||||
}
|
||||
|
||||
return StarsWithdrawConfiguration(minWithdrawAmount: minWithdrawAmount, maxPaidMediaAmount: maxPaidMediaAmount)
|
||||
} else {
|
||||
return .defaultValue
|
||||
}
|
||||
|
|
@ -742,7 +742,7 @@ public class StickerPickerScreen: ViewController {
|
|||
|
||||
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
|
||||
|
||||
let gallery = GalleryController(context: context, source: .standaloneMessage(message), streamSingleVideo: true, replaceRootController: { _, _ in
|
||||
let gallery = GalleryController(context: context, source: .standaloneMessage(message, nil), streamSingleVideo: true, replaceRootController: { _, _ in
|
||||
}, baseNavigationController: nil)
|
||||
gallery.setHintWillBePresentedInPreviewingContext(true)
|
||||
|
||||
|
|
|
|||
12
submodules/TelegramUI/Images.xcassets/Media Grid/Lock.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Media Grid/Lock.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "paidlock (3).pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Grid/Lock.imageset/paidlock (3).pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Media Grid/Lock.imageset/paidlock (3).pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Premium/Perk/MessageEffects.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Premium/Perk/MessageEffects.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "msgeffects.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Premium/Perk/MessageEffects.imageset/msgeffects.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Premium/Perk/MessageEffects.imageset/msgeffects.pdf
vendored
Normal file
Binary file not shown.
|
|
@ -901,42 +901,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #if DEBUG
|
||||
// if message.text == "#", let telegramImage = message.media.first(where: { $0 is TelegramMediaImage }) as? TelegramMediaImage {
|
||||
// let invoice = TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: "XTR", totalAmount: 100, startParam: "", extendedMedia: .preview(dimensions: telegramImage.representations.first?.dimensions ?? PixelDimensions(width: 1, height: 1), immediateThumbnailData: telegramImage.immediateThumbnailData, videoDuration: nil), flags: [], version: 0)
|
||||
//
|
||||
// let inputData = Promise<BotCheckoutController.InputData?>()
|
||||
// inputData.set(.single(
|
||||
// BotCheckoutController.InputData(
|
||||
// form: BotPaymentForm(id: 123, canSaveCredentials: false, passwordMissing: false, invoice: BotPaymentInvoice(isTest: false, requestedFields: [], currency: "XTR", prices: [BotPaymentPrice(label: "", amount: 100)], tip: nil, termsInfo: nil), paymentBotId: message.id.peerId, providerId: nil, url: nil, nativeProvider: nil, savedInfo: nil, savedCredentials: [], additionalPaymentMethods: []),
|
||||
// validatedFormInfo: nil,
|
||||
// botPeer: nil
|
||||
// )))
|
||||
// if invoice.currency == "XTR", let starsContext = strongSelf.context.starsContext {
|
||||
// let starsInputData = combineLatest(
|
||||
// inputData.get(),
|
||||
// starsContext.state
|
||||
// )
|
||||
// |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in
|
||||
// if let data, let state {
|
||||
// return (state, data.form, data.botPeer)
|
||||
// } else {
|
||||
// return nil
|
||||
// }
|
||||
// }
|
||||
// let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
// guard let strongSelf = self else {
|
||||
// return
|
||||
// }
|
||||
// let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .message(message.id), inputData: starsInputData, completion: { _ in })
|
||||
// strongSelf.push(controller)
|
||||
// })
|
||||
// }
|
||||
// return true
|
||||
// }
|
||||
// #endif
|
||||
|
||||
if let paidContent = media as? TelegramMediaPaidContent, let extendedMedia = paidContent.extendedMedia.first {
|
||||
switch extendedMedia {
|
||||
case .preview:
|
||||
|
|
@ -1291,7 +1255,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
standalone = true
|
||||
}
|
||||
|
||||
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: openChatLocation, chatFilterTag: chatFilterTag, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, standalone: standalone, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: {
|
||||
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: openChatLocation, chatFilterTag: chatFilterTag, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, mediaIndex: params.mediaIndex, standalone: standalone, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: {
|
||||
self?.chatDisplayNode.dismissInput()
|
||||
}, present: { c, a in
|
||||
self?.present(c, in: .window(.root), with: a, blockInteraction: true)
|
||||
|
|
@ -2670,7 +2634,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
return
|
||||
}
|
||||
let invoice = TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: "XTR", totalAmount: paidContent.amount, startParam: "", extendedMedia: .preview(dimensions:dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: nil), flags: [], version: 0)
|
||||
let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .message(messageId), inputData: starsInputData, completion: { _ in })
|
||||
let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .message(messageId), extendedMedia: paidContent.extendedMedia, inputData: starsInputData, completion: { _ in })
|
||||
strongSelf.push(controller)
|
||||
})
|
||||
}
|
||||
|
|
@ -2711,7 +2675,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .message(messageId), inputData: starsInputData, completion: { _ in })
|
||||
let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .message(messageId), extendedMedia: [], inputData: starsInputData, completion: { _ in })
|
||||
strongSelf.push(controller)
|
||||
})
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -638,6 +638,30 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|||
imageDimensions = dimensions.cgSize
|
||||
}
|
||||
break
|
||||
} else if let paidContent = media as? TelegramMediaPaidContent, let firstMedia = paidContent.extendedMedia.first {
|
||||
switch firstMedia {
|
||||
case let .preview(dimensions, immediateThumbnailData, _):
|
||||
let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
||||
if let dimensions {
|
||||
imageDimensions = dimensions.cgSize
|
||||
}
|
||||
updatedMediaReference = .standalone(media: thumbnailMedia)
|
||||
case let .full(fullMedia):
|
||||
updatedMediaReference = .message(message: MessageReference(message), media: fullMedia)
|
||||
if let image = fullMedia as? TelegramMediaImage {
|
||||
if let representation = largestRepresentationForPhoto(image) {
|
||||
imageDimensions = representation.dimensions.cgSize
|
||||
}
|
||||
break
|
||||
} else if let file = fullMedia as? TelegramMediaFile {
|
||||
if !file.isInstantVideo && !file.isSticker, let representation = largestImageRepresentation(file.previewRepresentations) {
|
||||
imageDimensions = representation.dimensions.cgSize
|
||||
} else if file.isAnimated, let dimensions = file.dimensions {
|
||||
imageDimensions = dimensions.cgSize
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -681,7 +705,11 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|||
if mediaUpdated {
|
||||
if let updatedMediaReference = updatedMediaReference, imageDimensions != nil {
|
||||
if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) {
|
||||
updateImageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, blurred: hasSpoiler)
|
||||
if imageReference.media.representations.isEmpty {
|
||||
updateImageSignal = chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, ignoreFullSize: true, synchronousLoad: true)
|
||||
} else {
|
||||
updateImageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, blurred: hasSpoiler)
|
||||
}
|
||||
} else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) {
|
||||
if fileReference.media.isAnimatedSticker {
|
||||
let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
|
|||
return true
|
||||
}
|
||||
|
||||
if let mediaData = chatMessageGalleryControllerData(context: params.context, chatLocation: params.chatLocation, chatFilterTag: params.chatFilterTag, chatLocationContextHolder: params.chatLocationContextHolder, message: params.message, navigationController: params.navigationController, standalone: params.standalone, reverseMessageGalleryOrder: params.reverseMessageGalleryOrder, mode: params.mode, source: params.gallerySource, synchronousLoad: false, actionInteraction: params.actionInteraction) {
|
||||
if let mediaData = chatMessageGalleryControllerData(context: params.context, chatLocation: params.chatLocation, chatFilterTag: params.chatFilterTag, chatLocationContextHolder: params.chatLocationContextHolder, message: params.message, mediaIndex: params.mediaIndex, navigationController: params.navigationController, standalone: params.standalone, reverseMessageGalleryOrder: params.reverseMessageGalleryOrder, mode: params.mode, source: params.gallerySource, synchronousLoad: false, actionInteraction: params.actionInteraction) {
|
||||
switch mediaData {
|
||||
case let .url(url):
|
||||
params.openUrl(url)
|
||||
|
|
|
|||
|
|
@ -837,7 +837,7 @@ func openResolvedUrlImpl(
|
|||
}
|
||||
}
|
||||
let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { _ in
|
||||
let controller = context.sharedContext.makeStarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: .slug(slug), inputData: starsInputData, completion: { _ in })
|
||||
let controller = context.sharedContext.makeStarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: .slug(slug), extendedMedia: [], inputData: starsInputData, completion: { _ in })
|
||||
navigationController.pushViewController(controller)
|
||||
})
|
||||
} else {
|
||||
|
|
@ -1072,7 +1072,7 @@ func openResolvedUrlImpl(
|
|||
},
|
||||
openMessage: { messageId in
|
||||
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId))
|
||||
|> deliverOnMainQueue).startStandalone(next: { peer in
|
||||
|> deliverOnMainQueue).startStandalone(next: { peer in
|
||||
guard let peer else {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ import BirthdayPickerScreen
|
|||
import StarsTransactionsScreen
|
||||
import StarsPurchaseScreen
|
||||
import StarsTransferScreen
|
||||
import StarsTransactionScreen
|
||||
import StarsWithdrawalScreen
|
||||
|
||||
private final class AccountUserInterfaceInUseContext {
|
||||
let subscribers = Bag<(Bool) -> Void>()
|
||||
|
|
@ -2072,6 +2074,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
mappedSource = .messageTags
|
||||
case .folderTags:
|
||||
mappedSource = .folderTags
|
||||
case .messageEffects:
|
||||
mappedSource = .messageEffects
|
||||
case .animatedEmoji:
|
||||
mappedSource = .animatedEmoji
|
||||
}
|
||||
|
|
@ -2128,6 +2132,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
mappedSubject = .messagePrivacy
|
||||
case .folderTags:
|
||||
mappedSubject = .folderTags
|
||||
case .messageEffects:
|
||||
mappedSubject = .messageEffects
|
||||
case .business:
|
||||
mappedSubject = .business
|
||||
buttonText = presentationData.strings.Chat_EmptyStateIntroFooterPremiumActionButton
|
||||
|
|
@ -2628,12 +2634,12 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
return StarsPurchaseScreen(context: context, starsContext: starsContext, options: options, peerId: peerId, requiredStars: requiredStars, modal: true, completion: completion)
|
||||
}
|
||||
|
||||
public func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController {
|
||||
return StarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: source, inputData: inputData, completion: completion)
|
||||
public func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController {
|
||||
return StarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: source, extendedMedia: extendedMedia, inputData: inputData, completion: completion)
|
||||
}
|
||||
|
||||
public func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, isAccount: Bool) -> ViewController {
|
||||
return StarsTransactionScreen(context: context, subject: .transaction(transaction, isAccount), action: {})
|
||||
public func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, peer: EnginePeer) -> ViewController {
|
||||
return StarsTransactionScreen(context: context, subject: .transaction(transaction, peer), action: {})
|
||||
}
|
||||
|
||||
public func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController {
|
||||
|
|
@ -2644,8 +2650,12 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
return StarsStatisticsScreen(context: context, peerId: peerId, revenueContext: revenueContext)
|
||||
}
|
||||
|
||||
public func makeStarsAmountScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController {
|
||||
return StarsWithdrawScreen(context: context, mode: .paidMedia, completion: completion)
|
||||
public func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController {
|
||||
return StarsWithdrawScreen(context: context, mode: .paidMedia(initialValue), completion: completion)
|
||||
}
|
||||
|
||||
public func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController {
|
||||
return StarsWithdrawScreen(context: context, mode: .withdraw(stats), completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -882,6 +882,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||
starsContext: starsContext,
|
||||
invoice: invoice,
|
||||
source: .slug(slug),
|
||||
extendedMedia: [],
|
||||
inputData: starsInputData,
|
||||
completion: { [weak self] paid in
|
||||
guard let self else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue