mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Merge commit '7d5d9ba7ba'
# Conflicts: # submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift
This commit is contained in:
commit
779b583927
125 changed files with 5809 additions and 1836 deletions
|
|
@ -10629,6 +10629,7 @@ Sorry for the inconvenience.";
|
|||
"MediaEditor.Shortcut.Location" = "Location";
|
||||
"MediaEditor.Shortcut.Reaction" = "Reaction";
|
||||
"MediaEditor.Shortcut.Audio" = "Audio";
|
||||
"MediaEditor.Shortcut.Link" = "Link";
|
||||
|
||||
"BoostGift.AdditionalPrizes" = "Additional Prizes";
|
||||
"BoostGift.AdditionalPrizesPlaceholder" = "Enter Your Prize";
|
||||
|
|
@ -12254,6 +12255,7 @@ Sorry for the inconvenience.";
|
|||
"Stars.Intro.Incoming" = "Incoming";
|
||||
"Stars.Intro.Outgoing" = "Outgoing";
|
||||
|
||||
"Stars.Intro.Transaction.MediaPurchase" = "Media Purchase";
|
||||
"Stars.Intro.Transaction.AppleTopUp.Title" = "Stars Top-Up";
|
||||
"Stars.Intro.Transaction.AppleTopUp.Subtitle" = "via App Store";
|
||||
"Stars.Intro.Transaction.GoogleTopUp.Title" = "Stars Top-Up";
|
||||
|
|
@ -12264,6 +12266,8 @@ Sorry for the inconvenience.";
|
|||
"Stars.Intro.Transaction.FragmentTopUp.Subtitle" = "via Fragment";
|
||||
"Stars.Intro.Transaction.FragmentWithdrawal.Title" = "Withdrawal";
|
||||
"Stars.Intro.Transaction.FragmentWithdrawal.Subtitle" = "via Fragment";
|
||||
"Stars.Intro.Transaction.TelegramAds.Title" = "Withdrawal";
|
||||
"Stars.Intro.Transaction.TelegramAds.Subtitle" = "via Telegram Ads";
|
||||
"Stars.Intro.Transaction.Unsupported.Title" = "Unsupported";
|
||||
"Stars.Intro.Transaction.Refund" = "Refund";
|
||||
|
||||
|
|
@ -12290,12 +12294,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";
|
||||
|
|
@ -12306,9 +12312,19 @@ Sorry for the inconvenience.";
|
|||
"Stars.Transaction.FragmentTopUp.Subtitle" = "Fragment";
|
||||
"Stars.Transaction.FragmentWithdrawal.Title" = "Stars Withdrawal";
|
||||
"Stars.Transaction.FragmentWithdrawal.Subtitle" = "Fragment";
|
||||
"Stars.Transaction.TelegramAds.Title" = "Stars Withdrawal";
|
||||
"Stars.Transaction.TelegramAds.Subtitle" = "Telegram Ads";
|
||||
"Stars.Transaction.Unsupported.Title" = "Unsupported";
|
||||
"Stars.Transaction.Refund" = "Refund";
|
||||
|
||||
"Stars.Transaction.Photos_1" = "%@ Photo";
|
||||
"Stars.Transaction.Photos_any" = "%@ Photos";
|
||||
"Stars.Transaction.Videos_1" = "%@ Video";
|
||||
"Stars.Transaction.Videos_any" = "%@ Videos";
|
||||
"Stars.Transaction.SinglePhoto" = "Photo";
|
||||
"Stars.Transaction.SingleVideo" = "Video";
|
||||
"Stars.Transaction.MediaAnd" = "%1$@ and %2$@";
|
||||
|
||||
"Stars.Transfer.Title" = "Confirm Your Purchase";
|
||||
"Stars.Transfer.Info" = "Do you want to buy **%1$@** in **%2$@** for **%3$@**?";
|
||||
"Stars.Transfer.Info.Stars_1" = "%@ Star";
|
||||
|
|
@ -12318,12 +12334,21 @@ Sorry for the inconvenience.";
|
|||
"Stars.Transfer.PurchasedText" = "You acquired **%1$@** in **%2$@** for **%3$@**.";
|
||||
"Stars.Transfer.Purchased.Stars_1" = "%@ Star";
|
||||
"Stars.Transfer.Purchased.Stars_any" = "%@ Stars";
|
||||
"Stars.Transfer.UnlockInfo" = "Do you want to unlock media for **%1$@**?";
|
||||
"Stars.Transfer.UnlockedText" = "You unlocked media for **%1$@**.";
|
||||
"Stars.Transfer.UnlockInfo" = "Do you want to unlock %1$@ in **%2$@** for **%3$@**?";
|
||||
|
||||
"Stars.Transfer.Balance" = "Balance";
|
||||
|
||||
"Stars.Transfer.Unavailable" = "Sorry, no star purchases are available from your country.";
|
||||
|
||||
"Stars.Transfer.Photos_1" = "%@ photo";
|
||||
"Stars.Transfer.Photos_any" = "%@ photos";
|
||||
"Stars.Transfer.Videos_1" = "%@ video";
|
||||
"Stars.Transfer.Videos_any" = "%@ videos";
|
||||
"Stars.Transfer.SinglePhoto" = "photo";
|
||||
"Stars.Transfer.SingleVideo" = "video";
|
||||
"Stars.Transfer.MediaAnd" = "%1$@ and %2$@";
|
||||
|
||||
"Settings.Stars" = "Your Stars";
|
||||
|
||||
"Chat.MessageEffectMenu.TitleAddEffect" = "Add an animated effect";
|
||||
|
|
@ -12348,16 +12373,19 @@ 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.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.Current" = "Total Balance";
|
||||
"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_URL" = "https://telegram.org/tos";
|
||||
"Stars.BotRevenue.Withdraw.WithdrawShort" = "Withdraw";
|
||||
"Stars.BotRevenue.Withdraw.BuyAds" = "Buy Ads";
|
||||
"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/bot-developers#6-2-2-tpa-balance";
|
||||
|
||||
"Stars.BotRevenue.Transactions.Title" = "Transaction History";
|
||||
|
||||
|
|
@ -12376,6 +12404,7 @@ Sorry for the inconvenience.";
|
|||
"Stars.PaidContent.AmountTitle" = "ENTER UNLOCK COST";
|
||||
"Stars.PaidContent.AmountPlaceholder" = "Stars to Unlock";
|
||||
"Stars.PaidContent.AmountInfo" = "Users will have to transfer this amount of Stars to your channel in order to view this media.\n[More about Stars >]()";
|
||||
"Stars.PaidContent.AmountInfo_URL" = "https://telegram.org/tos/stars";
|
||||
"Stars.PaidContent.Create" = "Make This Media Paid";
|
||||
|
||||
"MediaEditor.AddLink" = "LINK";
|
||||
|
|
@ -12401,3 +12430,33 @@ 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.BalanceStarsWithdrawShort" = "Withdraw";
|
||||
"Monetization.BalanceStarsBuyAds" = "Buy Ads";
|
||||
"Monetization.Balance.StarsInfo" = "You can collect rewards for Stars using Fragment, or use Stars to advertise your channel. [Learn More >]()";
|
||||
"Monetization.Balance.StarsInfo_URL" = "https://telegram.org";
|
||||
|
||||
"Monetization.StarsProceeds.Title" = "Rewards Overview";
|
||||
"Monetization.StarsProceeds.Available" = "Available Balance";
|
||||
"Monetization.StarsProceeds.Current" = "Total Balance";
|
||||
"Monetization.StarsProceeds.Total" = "Lifetime Proceeds";
|
||||
|
||||
"Premium.MessageEffects" = "Message Effects";
|
||||
"Premium.MessageEffectsInfo" = "Add over 500 animated effects to private messages.";
|
||||
|
||||
"Chat.UnlockMedia" = "Unlock for %@";
|
||||
|
||||
"Attachment.SendWithoutGrouping" = "Send Without Grouping";
|
||||
"Attachment.Paid.EditPrice" = "Edit Price";
|
||||
"Attachment.Paid.EditPrice.Stars_1" = "%@ Star";
|
||||
"Attachment.Paid.EditPrice.Stars_any" = "%@ Stars";
|
||||
"Attachment.Paid.Create" = "Make This Content Paid";
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -1069,6 +1069,7 @@ public enum FileMediaResourceMediaStatus: Equatable {
|
|||
}
|
||||
|
||||
public protocol ChatMessageItemNodeProtocol: ListViewItemNode {
|
||||
func makeProgress() -> Promise<Bool>?
|
||||
func targetReactionView(value: MessageReaction.Reaction) -> UIView?
|
||||
func targetForStoryTransition(id: StoryId) -> UIView?
|
||||
func contentFrame() -> CGRect
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1040,8 +1040,13 @@ public class AttachmentController: ViewController {
|
|||
|
||||
public override var isMinimized: Bool {
|
||||
didSet {
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||
transition.updateAlpha(node: self.node.dim, alpha: self.isMinimized ? 0.0 : 1.0)
|
||||
guard self.isMinimized != oldValue else {
|
||||
return
|
||||
}
|
||||
if !self.node.isDismissing {
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||
transition.updateAlpha(node: self.node.dim, alpha: self.isMinimized ? 0.0 : 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1044,8 +1044,7 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||
self.animateOutToEmpty = true
|
||||
self.environment?.controller()?.dismiss()
|
||||
|
||||
//TODO:localize
|
||||
let premiumController = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .animatedEmoji, forceDark: false, dismissed: nil)
|
||||
let premiumController = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .messageEffects, forceDark: false, dismissed: nil)
|
||||
component.openPremiumPaywall(premiumController)
|
||||
}
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ public func messageFileMediaResourceStatus(context: AccountContext, file: Telegr
|
|||
}
|
||||
}
|
||||
} else if let pendingStatus = pendingStatus {
|
||||
mediaStatus = .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: pendingStatus.progress))
|
||||
mediaStatus = .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: pendingStatus.progress.progress))
|
||||
} else {
|
||||
mediaStatus = .fetchStatus(EngineMediaResource.FetchStatus(resourceStatus))
|
||||
}
|
||||
|
|
@ -104,7 +104,7 @@ public func messageImageMediaResourceStatus(context: AccountContext, image: Tele
|
|||
|> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in
|
||||
let mediaStatus: FileMediaResourceMediaStatus
|
||||
if let pendingStatus = pendingStatus {
|
||||
mediaStatus = .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: pendingStatus.progress))
|
||||
mediaStatus = .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: pendingStatus.progress.progress))
|
||||
} else {
|
||||
mediaStatus = .fetchStatus(EngineMediaResource.FetchStatus(resourceStatus))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,7 +111,10 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
|
|||
}
|
||||
}
|
||||
for media in message.media {
|
||||
if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia {
|
||||
if let paidContent = media as? TelegramMediaPaidContent, let extendedMedia = paidContent.extendedMedia.first, case .full = extendedMedia {
|
||||
standalone = true
|
||||
galleryMedia = paidContent
|
||||
} else if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia {
|
||||
standalone = true
|
||||
galleryMedia = fullMedia
|
||||
} else if let action = media as? TelegramMediaAction {
|
||||
|
|
@ -238,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") {
|
||||
|
|
@ -277,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
|
||||
|
|
|
|||
|
|
@ -817,7 +817,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
|
|||
self.currentMessage = message
|
||||
|
||||
var displayInfo = displayInfo
|
||||
if Namespaces.Message.allNonRegular.contains(message.id.namespace) {
|
||||
if Namespaces.Message.allNonRegular.contains(message.id.namespace) || message.timestamp == 0 {
|
||||
displayInfo = false
|
||||
}
|
||||
|
||||
|
|
@ -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/")
|
||||
|
|
@ -1622,7 +1624,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
|
|||
|
||||
var hasExternalShare = true
|
||||
for media in currentMessage.media {
|
||||
if let invoice = media as? TelegramMediaInvoice, let _ = invoice.extendedMedia {
|
||||
if let _ = media as? TelegramMediaPaidContent {
|
||||
hasExternalShare = false
|
||||
break
|
||||
} else if let invoice = media as? TelegramMediaInvoice, let _ = invoice.extendedMedia {
|
||||
hasExternalShare = false
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,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:
|
||||
|
|
@ -83,7 +91,7 @@ private func mediaForMessage(message: Message) -> (Media, TelegramMediaImage?)?
|
|||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return []
|
||||
}
|
||||
|
||||
private let internalExtensions = Set<String>([
|
||||
|
|
@ -174,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,
|
||||
|
|
@ -196,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 || message.paidContent != nil
|
||||
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, entry.mediaIndex),
|
||||
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, entry.mediaIndex),
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -491,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)
|
||||
|
|
@ -524,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>()
|
||||
|
|
@ -648,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
|
||||
|
|
@ -730,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
|
||||
}
|
||||
}
|
||||
|
|
@ -773,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
|
||||
|
|
@ -1225,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()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1285,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1353,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
|
||||
|
|
@ -1374,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() {
|
||||
|
|
@ -1395,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 {
|
||||
|
|
@ -1422,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
|
||||
|
|
@ -1438,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
|
||||
|
|
@ -1468,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 {
|
||||
|
|
@ -1490,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
|
||||
|
|
@ -1549,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())
|
||||
|
|
@ -1557,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?()
|
||||
|
|
@ -1589,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())
|
||||
|
|
@ -1598,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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -228,15 +228,19 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture
|
|||
fromLeft = true
|
||||
}
|
||||
if let current = strongSelf.currentThumbnailContainerNode {
|
||||
strongSelf.currentThumbnailContainerNode = nil
|
||||
if thumbnailContainerVisible {
|
||||
current.animateOut(toRight: fromLeft)
|
||||
current.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak current] _ in
|
||||
current?.removeFromSupernode()
|
||||
})
|
||||
if let (navigationHeight, layout) = strongSelf.containerLayout, node == nil {
|
||||
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
strongSelf.currentThumbnailContainerNode = node
|
||||
if let node = node {
|
||||
strongSelf.currentThumbnailContainerNode = node
|
||||
strongSelf.insertSubnode(node, aboveSubnode: strongSelf.footerNode)
|
||||
if let (navigationHeight, layout) = strongSelf.containerLayout, thumbnailContainerVisible {
|
||||
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ public final class GalleryFooterNode: ASDisplayNode {
|
|||
}
|
||||
self.addSubnode(footerContentNode)
|
||||
}
|
||||
} else if let _ = self.currentThumbnailPanelHeight {
|
||||
self.currentThumbnailPanelHeight = thumbnailPanelHeight
|
||||
}
|
||||
|
||||
var animateOverlayIn = false
|
||||
|
|
|
|||
|
|
@ -91,6 +91,8 @@ final class ChatMediaGalleryThumbnailItem: GalleryThumbnailItem {
|
|||
case let .video(fileReference):
|
||||
if let representation = largestImageRepresentation(fileReference.media.previewRepresentations) {
|
||||
return (mediaGridMessageVideo(postbox: self.account.postbox, userLocation: self.userLocation, videoReference: fileReference), representation.dimensions.cgSize)
|
||||
} else if let dimensions = fileReference.media.dimensions {
|
||||
return (mediaGridMessageVideo(postbox: self.account.postbox, userLocation: self.userLocation, videoReference: fileReference), dimensions.cgSize)
|
||||
} else {
|
||||
return (.single({ _ in return nil }), CGSize(width: 128.0, height: 128.0))
|
||||
}
|
||||
|
|
@ -112,6 +114,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 +124,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,7 +144,12 @@ 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 invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia, let image = fullMedia as? TelegramMediaImage {
|
||||
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 {
|
||||
node.setImage(userLocation: .peer(self.message.id.peerId), imageReference: .message(message: MessageReference(self.message), media: image))
|
||||
|
|
@ -182,7 +191,18 @@ class ChatImageGalleryItem: GalleryItem {
|
|||
}
|
||||
|
||||
func thumbnailItem() -> (Int64, GalleryThumbnailItem)? {
|
||||
if let id = self.message.groupInfo?.stableId {
|
||||
if let paidContent = self.message.paidContent {
|
||||
var mediaReference: AnyMediaReference?
|
||||
let mediaIndex = self.mediaIndex ?? 0
|
||||
if case let .full(fullMedia) = paidContent.extendedMedia[Int(mediaIndex)], let m = fullMedia as? TelegramMediaImage {
|
||||
mediaReference = .message(message: MessageReference(self.message), media: m)
|
||||
}
|
||||
if let mediaReference = mediaReference {
|
||||
if let item = ChatMediaGalleryThumbnailItem(account: self.context.account, userLocation: .peer(self.message.id.peerId), mediaReference: mediaReference) {
|
||||
return (0, item)
|
||||
}
|
||||
}
|
||||
} else if let id = self.message.groupInfo?.stableId {
|
||||
var mediaReference: AnyMediaReference?
|
||||
for m in self.message.media {
|
||||
if let m = m as? TelegramMediaImage {
|
||||
|
|
@ -238,6 +258,16 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
|
||||
private var currentSpeechHolder: SpeechSynthesizerHolder?
|
||||
|
||||
override var baseNavigationController: () -> NavigationController? {
|
||||
didSet {
|
||||
if let _ = self.baseNavigationController() {
|
||||
self.moreBarButton.isHidden = false
|
||||
} else {
|
||||
self.moreBarButton.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||
self.context = context
|
||||
self.presentationData = presentationData
|
||||
|
|
@ -336,7 +366,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
self.translateToLanguage = translateToLanguage
|
||||
self.peerIsCopyProtected = peerIsCopyProtected
|
||||
self.isSecret = isSecret
|
||||
self.imageNode.captureProtected = message.id.peerId.namespace == Namespaces.Peer.SecretChat || message.isCopyProtected() || peerIsCopyProtected || isSecret
|
||||
self.imageNode.captureProtected = message.id.peerId.namespace == Namespaces.Peer.SecretChat || message.isCopyProtected() || peerIsCopyProtected || isSecret || message.paidContent != nil
|
||||
self.footerContentNode.setMessage(message, displayInfo: displayInfo, translateToLanguage: translateToLanguage, peerIsCopyProtected: peerIsCopyProtected)
|
||||
}
|
||||
|
||||
|
|
@ -526,7 +556,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)
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import TextFormat
|
|||
import SliderContextItem
|
||||
|
||||
public enum UniversalVideoGalleryItemContentInfo {
|
||||
case message(Message)
|
||||
case message(Message, Int?)
|
||||
case webPage(TelegramMediaWebpage, Media, ((@escaping () -> GalleryTransitionArguments?, NavigationController?, (ViewController, Any?) -> Void) -> Void)?)
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ public class UniversalVideoGalleryItem: GalleryItem {
|
|||
|
||||
node.setupItem(self)
|
||||
|
||||
if self.displayInfoOnTop, case let .message(message) = self.contentInfo {
|
||||
if self.displayInfoOnTop, case let .message(message, _) = self.contentInfo {
|
||||
node.titleContentView?.setMessage(message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId)
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ public class UniversalVideoGalleryItem: GalleryItem {
|
|||
|
||||
node.setupItem(self)
|
||||
|
||||
if self.displayInfoOnTop, case let .message(message) = self.contentInfo {
|
||||
if self.displayInfoOnTop, case let .message(message, _) = self.contentInfo {
|
||||
node.titleContentView?.setMessage(message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId)
|
||||
}
|
||||
}
|
||||
|
|
@ -118,8 +118,19 @@ public class UniversalVideoGalleryItem: GalleryItem {
|
|||
guard let contentInfo = self.contentInfo else {
|
||||
return nil
|
||||
}
|
||||
if case let .message(message) = contentInfo {
|
||||
if let id = message.groupInfo?.stableId {
|
||||
if case let .message(message, mediaIndex) = contentInfo {
|
||||
if let paidContent = message.paidContent {
|
||||
var mediaReference: AnyMediaReference?
|
||||
let mediaIndex = mediaIndex ?? 0
|
||||
if case let .full(fullMedia) = paidContent.extendedMedia[Int(mediaIndex)], let m = fullMedia as? TelegramMediaFile {
|
||||
mediaReference = .message(message: MessageReference(message), media: m)
|
||||
}
|
||||
if let mediaReference = mediaReference {
|
||||
if let item = ChatMediaGalleryThumbnailItem(account: self.context.account, userLocation: .peer(message.id.peerId), mediaReference: mediaReference) {
|
||||
return (0, item)
|
||||
}
|
||||
}
|
||||
} else if let id = message.groupInfo?.stableId {
|
||||
var mediaReference: AnyMediaReference?
|
||||
for m in message.media {
|
||||
if let m = m as? TelegramMediaImage {
|
||||
|
|
@ -1183,8 +1194,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
var mediaFileStatus: Signal<MediaResourceStatus?, NoError> = .single(nil)
|
||||
|
||||
var hintSeekable = false
|
||||
if let contentInfo = item.contentInfo, case let .message(message) = contentInfo {
|
||||
if Namespaces.Message.allNonRegular.contains(message.id.namespace) {
|
||||
if let contentInfo = item.contentInfo, case let .message(message, _) = contentInfo {
|
||||
if Namespaces.Message.allNonRegular.contains(message.id.namespace) || message.id.namespace == Namespaces.Message.Local {
|
||||
disablePictureInPicture = true
|
||||
} else {
|
||||
let throttledSignal = videoNode.status
|
||||
|
|
@ -1432,7 +1443,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
self.hasPictureInPicture = false
|
||||
}
|
||||
|
||||
if let contentInfo = item.contentInfo, case let .message(message) = contentInfo {
|
||||
if let contentInfo = item.contentInfo, case let .message(message, mediaIndex) = contentInfo {
|
||||
var file: TelegramMediaFile?
|
||||
for m in message.media {
|
||||
if let m = m as? TelegramMediaFile, m.isVideo {
|
||||
|
|
@ -1441,6 +1452,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
} else if let m = m as? TelegramMediaWebpage, case let .Loaded(content) = m.content, let f = content.file, f.isVideo {
|
||||
file = f
|
||||
break
|
||||
} else if let paidContent = message.paidContent {
|
||||
let mediaIndex = mediaIndex ?? 0
|
||||
let media = paidContent.extendedMedia[mediaIndex]
|
||||
if case let .full(fullMedia) = media, let m = fullMedia as? TelegramMediaFile {
|
||||
file = m
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1450,6 +1468,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
} else if let file = file, !file.isAnimated {
|
||||
hasMoreButton = true
|
||||
}
|
||||
|
||||
if let _ = message.paidContent, message.id.namespace == Namespaces.Message.Local {
|
||||
hasMoreButton = false
|
||||
}
|
||||
|
||||
if hasMoreButton {
|
||||
let moreMenuItem = UIBarButtonItem(customDisplayNode: self.moreBarButton)!
|
||||
|
|
@ -1506,7 +1528,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
|
||||
if let contentInfo = item.contentInfo {
|
||||
switch contentInfo {
|
||||
case let .message(message):
|
||||
case let .message(message, _):
|
||||
self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop, peerIsCopyProtected: item.peerIsCopyProtected)
|
||||
case let .webPage(webPage, media, _):
|
||||
self.footerContentNode.setWebPage(webPage, media: media)
|
||||
|
|
@ -1558,7 +1580,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
isLocal = true
|
||||
}
|
||||
var isStreamable = false
|
||||
if let contentInfo = item.contentInfo, case let .message(message) = contentInfo {
|
||||
if let contentInfo = item.contentInfo, case let .message(message, _) = contentInfo {
|
||||
isStreamable = isMediaStreamable(message: message, media: content.fileReference.media)
|
||||
} else {
|
||||
isStreamable = isMediaStreamable(media: content.fileReference.media)
|
||||
|
|
@ -2131,7 +2153,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
}
|
||||
|
||||
switch contentInfo {
|
||||
case let .message(message):
|
||||
case let .message(message, _):
|
||||
let gallery = GalleryController(context: context, source: .peerMessagesAtId(messageId: message.id, chatLocation: .peer(id: message.id.peerId), customTag: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil)), playbackRate: playbackRate, replaceRootController: { controller, ready in
|
||||
if let baseNavigationController = baseNavigationController {
|
||||
baseNavigationController.replaceTopController(controller, animated: false, ready: ready)
|
||||
|
|
@ -2197,7 +2219,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
@objc func pictureInPictureButtonPressed() {
|
||||
var isNativePictureInPictureSupported = false
|
||||
switch self.item?.contentInfo {
|
||||
case let .message(message):
|
||||
case let .message(message, _):
|
||||
for media in message.media {
|
||||
if let media = media as? TelegramMediaFile, media.isVideo {
|
||||
if message.id.namespace == Namespaces.Message.Cloud {
|
||||
|
|
@ -2227,7 +2249,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
|
||||
var hiddenMedia: (MessageId, Media)? = nil
|
||||
switch item.contentInfo {
|
||||
case let .message(message):
|
||||
case let .message(message, _):
|
||||
for media in message.media {
|
||||
if let media = media as? TelegramMediaImage {
|
||||
hiddenMedia = (message.id, media)
|
||||
|
|
@ -2255,7 +2277,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
}
|
||||
|
||||
switch contentInfo {
|
||||
case let .message(message):
|
||||
case let .message(message, _):
|
||||
let gallery = GalleryController(context: context, source: .peerMessagesAtId(messageId: message.id, chatLocation: .peer(id: message.id.peerId), customTag: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil)), playbackRate: playbackRate, replaceRootController: { [weak baseNavigationController] controller, ready in
|
||||
if let baseNavigationController = baseNavigationController {
|
||||
baseNavigationController.replaceTopController(controller, animated: false, ready: ready)
|
||||
|
|
@ -2288,7 +2310,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
var expandImpl: (() -> Void)?
|
||||
|
||||
let shouldBeDismissed: Signal<Bool, NoError>
|
||||
if let contentInfo = item.contentInfo, case let .message(message) = contentInfo {
|
||||
if let contentInfo = item.contentInfo, case let .message(message, _) = contentInfo {
|
||||
shouldBeDismissed = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: message.id))
|
||||
|> map { message -> Bool in
|
||||
if let _ = message {
|
||||
|
|
@ -2316,8 +2338,15 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
}
|
||||
|
||||
switch contentInfo {
|
||||
case let .message(message):
|
||||
let gallery = GalleryController(context: context, source: .peerMessagesAtId(messageId: message.id, chatLocation: .peer(id: message.id.peerId), customTag: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil)), playbackRate: playbackRate, replaceRootController: { controller, ready in
|
||||
case let .message(message, messageIndex):
|
||||
let source: GalleryControllerItemSource
|
||||
if let _ = message.paidContent {
|
||||
source = .standaloneMessage(message, messageIndex)
|
||||
} else {
|
||||
source = .peerMessagesAtId(messageId: message.id, chatLocation: .peer(id: message.id.peerId), customTag: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil))
|
||||
}
|
||||
|
||||
let gallery = GalleryController(context: context, source: source, playbackRate: playbackRate, replaceRootController: { controller, ready in
|
||||
if let baseNavigationController = baseNavigationController {
|
||||
baseNavigationController.replaceTopController(controller, animated: false, ready: ready)
|
||||
}
|
||||
|
|
@ -2385,11 +2414,17 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
guard let item = self.item else {
|
||||
return nil
|
||||
}
|
||||
if let contentInfo = item.contentInfo, case let .message(message) = contentInfo {
|
||||
if let contentInfo = item.contentInfo, case let .message(message, mediaIndex) = contentInfo {
|
||||
var file: TelegramMediaFile?
|
||||
var isWebpage = false
|
||||
for m in message.media {
|
||||
if let m = m as? TelegramMediaFile, m.isVideo {
|
||||
if let paidContent = m as? TelegramMediaPaidContent {
|
||||
let media = paidContent.extendedMedia[mediaIndex ?? 0]
|
||||
if case let .full(fullMedia) = media, let fullMedia = fullMedia as? TelegramMediaFile, fullMedia.isVideo {
|
||||
file = fullMedia
|
||||
}
|
||||
break
|
||||
} else if let m = m as? TelegramMediaFile, m.isVideo {
|
||||
file = m
|
||||
break
|
||||
} else if let m = m as? TelegramMediaWebpage, case let .Loaded(content) = m.content {
|
||||
|
|
@ -2573,7 +2608,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 {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
- (instancetype)initWithGroupingAllowed:(bool)allowGrouping selectionLimit:(int)selectionLimit;
|
||||
|
||||
@property (nonatomic, readonly) bool allowGrouping;
|
||||
@property (nonatomic, readonly) int selectionLimit;
|
||||
@property (nonatomic, assign) int selectionLimit;
|
||||
@property (nonatomic, copy) void (^selectionLimitExceeded)(void);
|
||||
@property (nonatomic, copy) bool (^attemptSelectingItem)(id<TGMediaSelectableItem>);
|
||||
|
||||
|
|
|
|||
|
|
@ -942,6 +942,7 @@
|
|||
NSInteger num = 0;
|
||||
bool grouping = selectionContext.grouping;
|
||||
|
||||
NSNumber *price;
|
||||
bool hasAnyTimers = false;
|
||||
if (editingContext != nil || grouping)
|
||||
{
|
||||
|
|
@ -950,6 +951,9 @@
|
|||
if ([editingContext timerForItem:asset] != nil) {
|
||||
hasAnyTimers = true;
|
||||
}
|
||||
if (price == nil) {
|
||||
price = [editingContext priceForItem:asset];
|
||||
}
|
||||
id<TGMediaEditAdjustments> adjustments = [editingContext adjustmentsForItem:asset];
|
||||
if ([adjustments isKindOfClass:[TGVideoEditAdjustments class]]) {
|
||||
TGVideoEditAdjustments *videoAdjustments = (TGVideoEditAdjustments *)adjustments;
|
||||
|
|
@ -1057,6 +1061,9 @@
|
|||
else if (groupedId != nil && !hasAnyTimers)
|
||||
dict[@"groupedId"] = groupedId;
|
||||
|
||||
if (price != nil)
|
||||
dict[@"price"] = price;
|
||||
|
||||
if (spoiler) {
|
||||
dict[@"spoiler"] = @true;
|
||||
}
|
||||
|
|
@ -1137,6 +1144,9 @@
|
|||
else if (groupedId != nil && !hasAnyTimers)
|
||||
dict[@"groupedId"] = groupedId;
|
||||
|
||||
if (price != nil)
|
||||
dict[@"price"] = price;
|
||||
|
||||
if (spoiler) {
|
||||
dict[@"spoiler"] = @true;
|
||||
}
|
||||
|
|
@ -1261,6 +1271,9 @@
|
|||
if (groupedId != nil)
|
||||
dict[@"groupedId"] = groupedId;
|
||||
|
||||
if (price != nil)
|
||||
dict[@"price"] = price;
|
||||
|
||||
if (spoiler) {
|
||||
dict[@"spoiler"] = @true;
|
||||
}
|
||||
|
|
@ -1334,6 +1347,9 @@
|
|||
else if (groupedId != nil && !hasAnyTimers)
|
||||
dict[@"groupedId"] = groupedId;
|
||||
|
||||
if (price != nil)
|
||||
dict[@"price"] = price;
|
||||
|
||||
if (spoiler) {
|
||||
dict[@"spoiler"] = @true;
|
||||
}
|
||||
|
|
@ -1415,6 +1431,9 @@
|
|||
if (timer != nil)
|
||||
dict[@"timer"] = timer;
|
||||
|
||||
if (price != nil)
|
||||
dict[@"price"] = price;
|
||||
|
||||
if (spoiler) {
|
||||
dict[@"spoiler"] = @true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,13 +134,15 @@ private final class LegacyAssetItemWrapper: NSObject {
|
|||
let item: LegacyAssetItem
|
||||
let timer: Int?
|
||||
let spoiler: Bool?
|
||||
let price: Int64?
|
||||
let groupedId: Int64?
|
||||
let uniqueId: String?
|
||||
|
||||
init(item: LegacyAssetItem, timer: Int?, spoiler: Bool?, groupedId: Int64?, uniqueId: String?) {
|
||||
init(item: LegacyAssetItem, timer: Int?, spoiler: Bool?, price: Int64?, groupedId: Int64?, uniqueId: String?) {
|
||||
self.item = item
|
||||
self.timer = timer
|
||||
self.spoiler = spoiler
|
||||
self.price = price
|
||||
self.groupedId = groupedId
|
||||
self.uniqueId = uniqueId
|
||||
|
||||
|
|
@ -159,6 +161,9 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str
|
|||
return nil
|
||||
}
|
||||
} ?? []
|
||||
|
||||
let price = dict["price"] as? Int64
|
||||
|
||||
if (dict["type"] as! NSString) == "editedPhoto" || (dict["type"] as! NSString) == "capturedPhoto" {
|
||||
let image = dict["image"] as! UIImage
|
||||
let thumbnail = dict["previewImage"] as? UIImage
|
||||
|
|
@ -168,10 +173,10 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str
|
|||
let url: String? = (dict["url"] as? String) ?? (dict["url"] as? URL)?.path
|
||||
if let url = url {
|
||||
let dimensions = image.size
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: 4.0), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: false, asAnimation: true, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: 4.0), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: false, asAnimation: true, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, price: price, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
}
|
||||
} else {
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .image(image), thumbnail: thumbnail, caption: caption, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .image(image), thumbnail: thumbnail, caption: caption, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, price: price, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
}
|
||||
return result
|
||||
} else if (dict["type"] as! NSString) == "cloudPhoto" {
|
||||
|
|
@ -192,9 +197,9 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str
|
|||
name = customName
|
||||
}
|
||||
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .asset(asset.backingAsset), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: nil, spoiler: nil, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .asset(asset.backingAsset), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: nil, spoiler: nil, price: price, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
} else {
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .asset(asset.backingAsset), thumbnail: thumbnail, caption: caption, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .asset(asset.backingAsset), thumbnail: thumbnail, caption: caption, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, price: price, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
}
|
||||
return result
|
||||
} else if (dict["type"] as! NSString) == "file" {
|
||||
|
|
@ -215,12 +220,12 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str
|
|||
let dimensions = (dict["dimensions"]! as AnyObject).cgSizeValue!
|
||||
let duration = (dict["duration"]! as AnyObject).doubleValue!
|
||||
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: tempFileUrl.path, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: nil, caption: caption, asFile: false, asAnimation: true, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: tempFileUrl.path, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: nil, caption: caption, asFile: false, asAnimation: true, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, price: price, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
return result
|
||||
}
|
||||
|
||||
var result: [AnyHashable: Any] = [:]
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .tempFile(tempFileUrl.path), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .tempFile(tempFileUrl.path), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, price: price, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
return result
|
||||
}
|
||||
} else if (dict["type"] as! NSString) == "video" {
|
||||
|
|
@ -232,13 +237,13 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str
|
|||
|
||||
if let asset = dict["asset"] as? TGMediaAsset {
|
||||
var result: [AnyHashable: Any] = [:]
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .asset(asset), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .asset(asset), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, price: price, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
return result
|
||||
} else if let url = (dict["url"] as? String) ?? (dict["url"] as? URL)?.absoluteString {
|
||||
let dimensions = (dict["dimensions"]! as AnyObject).cgSizeValue!
|
||||
let duration = (dict["duration"]! as AnyObject).doubleValue!
|
||||
var result: [AnyHashable: Any] = [:]
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, price: price, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
return result
|
||||
}
|
||||
} else if (dict["type"] as! NSString) == "cameraVideo" {
|
||||
|
|
@ -254,7 +259,7 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str
|
|||
let dimensions = previewImage.pixelSize()
|
||||
let duration = (dict["duration"]! as AnyObject).doubleValue!
|
||||
var result: [AnyHashable: Any] = [:]
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, price: price, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -356,6 +361,15 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A
|
|||
return Signal { subscriber in
|
||||
let disposable = SSignal.combineSignals(signals).start(next: { anyValues in
|
||||
var messages: [LegacyAssetPickerEnqueueMessage] = []
|
||||
|
||||
struct EnqueuePaidMessage {
|
||||
var price: Int64
|
||||
var text: String
|
||||
var entities: [MessageTextEntity]
|
||||
var media: [Media]
|
||||
}
|
||||
|
||||
var paidMessage: EnqueuePaidMessage?
|
||||
|
||||
outer: for item in (anyValues as! NSArray) {
|
||||
if let item = (item as? NSDictionary)?.object(forKey: "item") as? LegacyAssetItemWrapper {
|
||||
|
|
@ -436,7 +450,26 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A
|
|||
}
|
||||
}
|
||||
}
|
||||
messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: false))
|
||||
|
||||
if let price = item.price {
|
||||
if var current = paidMessage {
|
||||
if current.text.isEmpty && !text.string.isEmpty {
|
||||
current.text = text.string
|
||||
current.entities = entities
|
||||
}
|
||||
current.media.append(media)
|
||||
paidMessage = current
|
||||
} else {
|
||||
paidMessage = EnqueuePaidMessage(
|
||||
price: price,
|
||||
text: text.string,
|
||||
entities: entities,
|
||||
media: [media]
|
||||
)
|
||||
}
|
||||
} else {
|
||||
messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: false))
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .asset(asset):
|
||||
|
|
@ -510,7 +543,25 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A
|
|||
}
|
||||
}
|
||||
|
||||
messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: false))
|
||||
if let price = item.price {
|
||||
if var current = paidMessage {
|
||||
if current.text.isEmpty && !text.string.isEmpty {
|
||||
current.text = text.string
|
||||
current.entities = entities
|
||||
}
|
||||
current.media.append(media)
|
||||
paidMessage = current
|
||||
} else {
|
||||
paidMessage = EnqueuePaidMessage(
|
||||
price: price,
|
||||
text: text.string,
|
||||
entities: entities,
|
||||
media: [media]
|
||||
)
|
||||
}
|
||||
} else {
|
||||
messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -560,7 +611,25 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A
|
|||
}
|
||||
}
|
||||
|
||||
messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: false))
|
||||
if let price = item.price {
|
||||
if var current = paidMessage {
|
||||
if current.text.isEmpty && !text.string.isEmpty {
|
||||
current.text = text.string
|
||||
current.entities = entities
|
||||
}
|
||||
current.media.append(media)
|
||||
paidMessage = current
|
||||
} else {
|
||||
paidMessage = EnqueuePaidMessage(
|
||||
price: price,
|
||||
text: text.string,
|
||||
entities: entities,
|
||||
media: [media]
|
||||
)
|
||||
}
|
||||
} else {
|
||||
messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: false))
|
||||
}
|
||||
}
|
||||
case .tempFile:
|
||||
break
|
||||
|
|
@ -612,7 +681,25 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A
|
|||
}
|
||||
}
|
||||
|
||||
messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: true))
|
||||
if let price = item.price {
|
||||
if var current = paidMessage {
|
||||
if current.text.isEmpty && !text.string.isEmpty {
|
||||
current.text = text.string
|
||||
current.entities = entities
|
||||
}
|
||||
current.media.append(media)
|
||||
paidMessage = current
|
||||
} else {
|
||||
paidMessage = EnqueuePaidMessage(
|
||||
price: price,
|
||||
text: text.string,
|
||||
entities: entities,
|
||||
media: [media]
|
||||
)
|
||||
}
|
||||
} else {
|
||||
messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: true))
|
||||
}
|
||||
case let .asset(asset):
|
||||
var randomId: Int64 = 0
|
||||
arc4random_buf(&randomId, 8)
|
||||
|
|
@ -647,7 +734,25 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A
|
|||
}
|
||||
}
|
||||
|
||||
messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: true))
|
||||
if let price = item.price {
|
||||
if var current = paidMessage {
|
||||
if current.text.isEmpty && !text.string.isEmpty {
|
||||
current.text = text.string
|
||||
current.entities = entities
|
||||
}
|
||||
current.media.append(media)
|
||||
paidMessage = current
|
||||
} else {
|
||||
paidMessage = EnqueuePaidMessage(
|
||||
price: price,
|
||||
text: text.string,
|
||||
entities: entities,
|
||||
media: [media]
|
||||
)
|
||||
}
|
||||
} else {
|
||||
messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: true))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
@ -822,11 +927,58 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A
|
|||
}
|
||||
}
|
||||
|
||||
messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: asFile))
|
||||
if let price = item.price {
|
||||
if var current = paidMessage {
|
||||
if current.text.isEmpty && !text.string.isEmpty {
|
||||
current.text = text.string
|
||||
current.entities = entities
|
||||
}
|
||||
current.media.append(media)
|
||||
paidMessage = current
|
||||
} else {
|
||||
paidMessage = EnqueuePaidMessage(
|
||||
price: price,
|
||||
text: text.string,
|
||||
entities: entities,
|
||||
media: [media]
|
||||
)
|
||||
}
|
||||
} else {
|
||||
messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: asFile))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let paidMessage {
|
||||
var attributes: [MessageAttribute] = []
|
||||
if !paidMessage.entities.isEmpty {
|
||||
attributes.append(TextEntitiesMessageAttribute(entities: paidMessage.entities))
|
||||
}
|
||||
messages.append(
|
||||
LegacyAssetPickerEnqueueMessage(
|
||||
message: .message(
|
||||
text: paidMessage.text,
|
||||
attributes: attributes,
|
||||
inlineStickers: [:],
|
||||
mediaReference: .standalone(
|
||||
media: TelegramMediaPaidContent(
|
||||
amount: paidMessage.price,
|
||||
extendedMedia: paidMessage.media.map { .full(media: $0) }
|
||||
)),
|
||||
threadId: nil,
|
||||
replyToMessageId: nil,
|
||||
replyToStoryId: nil,
|
||||
localGroupingKey: nil,
|
||||
correlationId: nil,
|
||||
bubbleUpEmojiOrStickersets: []
|
||||
),
|
||||
uniqueId: nil,
|
||||
isFile: false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
subscriber.putNext(messages)
|
||||
subscriber.putCompletion()
|
||||
}, error: { _ in
|
||||
|
|
|
|||
|
|
@ -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,45 +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)
|
||||
}
|
||||
self.priceNode = priceNode
|
||||
|
||||
if animated {
|
||||
priceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
backgroundNode.addSubnode(labelNode)
|
||||
backgroundNode.addSubnode(iconNode)
|
||||
}
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1838,6 +1838,21 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
}
|
||||
}
|
||||
|
||||
if let selectionContext = self.interaction?.selectionState, let editingContext = self.interaction?.editingState {
|
||||
var price: Int64?
|
||||
for case let item as TGMediaEditableItem in selectionContext.selectedItems() {
|
||||
if price == nil, let itemPrice = editingContext.price(for: item) as? Int64 {
|
||||
price = itemPrice
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let price, let item = item as? TGMediaEditableItem {
|
||||
editingContext.setPrice(NSNumber(value: price), for: item)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -2013,7 +2028,6 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
|
||||
self.updateSelectionState(count: Int32(selectionContext.count()))
|
||||
|
||||
|
||||
if case let .assets(_, mode) = self.subject, case .createSticker = mode {
|
||||
let _ = cutoutAvailability(context: context).startStandalone()
|
||||
}
|
||||
|
|
@ -2434,9 +2448,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
}
|
||||
|
||||
var hasSpoilers = false
|
||||
var price: Int64?
|
||||
var hasGeneric = false
|
||||
if let selectionContext = self.interaction?.selectionState, let editingContext = self.interaction?.editingState {
|
||||
for case let item as TGMediaEditableItem in selectionContext.selectedItems() {
|
||||
if price == nil, let itemPrice = editingContext.price(for: item) as? Int64 {
|
||||
price = itemPrice
|
||||
}
|
||||
if editingContext.spoiler(for: item) {
|
||||
hasSpoilers = true
|
||||
} else {
|
||||
|
|
@ -2456,8 +2474,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
)
|
||||
|> deliverOnMainQueue
|
||||
|> map { [weak self] grouped, isCaptionAboveMediaAvailable -> ContextController.Items in
|
||||
guard let self else {
|
||||
return ContextController.Items(content: .list([]))
|
||||
}
|
||||
var items: [ContextMenuItem] = []
|
||||
if !hasSpoilers {
|
||||
if !hasSpoilers && price == nil {
|
||||
items.append(.action(ContextMenuActionItem(text: selectionCount > 1 ? strings.Attachment_SendAsFiles : strings.Attachment_SendAsFile, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/File"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, f in
|
||||
|
|
@ -2466,42 +2487,21 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
self?.controllerNode.send(asFile: true, silently: false, scheduleTime: nil, animated: true, parameters: nil, completion: {})
|
||||
})))
|
||||
}
|
||||
if selectionCount > 1 {
|
||||
// items.append(.action(ContextMenuActionItem(text: "Send Without Grouping", icon: { theme in
|
||||
// return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/GroupingOff"), color: theme.contextMenu.primaryColor)
|
||||
// }, action: { [weak self] _, f in
|
||||
// f(.default)
|
||||
//
|
||||
// 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)
|
||||
if selectionCount > 1, price == nil {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.Attachment_SendWithoutGrouping, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/GroupingOff"), 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
|
||||
self?.controllerNode.send(asFile: false, silently: false, scheduleTime: nil, animated: true, parameters: nil, completion: {})
|
||||
})))
|
||||
}
|
||||
|
||||
let isPaidAvailable = !"".isEmpty
|
||||
var isPaidAvailable = false
|
||||
if let peer = self.peer, case let .channel(channel) = peer, case .broadcast = channel.info, selectionCount <= 10 {
|
||||
isPaidAvailable = true
|
||||
}
|
||||
if isSpoilerAvailable || isPaidAvailable || (selectionCount > 0 && isCaptionAboveMediaAvailable) {
|
||||
if !items.isEmpty {
|
||||
items.append(.separator)
|
||||
|
|
@ -2509,7 +2509,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
|
||||
if isCaptionAboveMediaAvailable {
|
||||
var mediaCaptionIsAbove = false
|
||||
if let interaction = self?.interaction {
|
||||
if let interaction = self.interaction {
|
||||
mediaCaptionIsAbove = interaction.captionIsAboveMedia
|
||||
}
|
||||
|
||||
|
|
@ -2526,7 +2526,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
}
|
||||
})))
|
||||
}
|
||||
if isSpoilerAvailable {
|
||||
if isSpoilerAvailable && price == nil {
|
||||
items.append(.action(ContextMenuActionItem(text: hasGeneric ? strings.Attachment_EnableSpoiler : strings.Attachment_DisableSpoiler, icon: { _ in return nil }, iconAnimation: ContextMenuActionItem.IconAnimation(
|
||||
name: "anim_spoiler",
|
||||
loop: true
|
||||
|
|
@ -2544,7 +2544,16 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
})))
|
||||
}
|
||||
if isPaidAvailable {
|
||||
items.append(.action(ContextMenuActionItem(text: "Make This Content Paid", icon: { theme in
|
||||
let title: String
|
||||
let titleLayout: ContextMenuActionItemTextLayout
|
||||
if let price {
|
||||
title = strings.Attachment_Paid_EditPrice
|
||||
titleLayout = .secondLineWithValue(strings.Attachment_Paid_EditPrice_Stars(Int32(price)))
|
||||
} else {
|
||||
title = strings.Attachment_Paid_Create
|
||||
titleLayout = .singleLine
|
||||
}
|
||||
items.append(.action(ContextMenuActionItem(text: title, textLayout: titleLayout, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Paid"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
|
|
@ -2552,11 +2561,12 @@ 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
|
||||
}
|
||||
if let selectionContext = strongSelf.interaction?.selectionState, let editingContext = strongSelf.interaction?.editingState {
|
||||
selectionContext.selectionLimit = 10
|
||||
for case let item as TGMediaEditableItem in selectionContext.selectedItems() {
|
||||
editingContext.setPrice(NSNumber(value: amount), for: item)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,12 +130,27 @@ private class MediaPickerSelectedItemNode: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
|
||||
self.spoilerDisposable.set((spoilerSignal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] hasSpoiler in
|
||||
let priceSignal = Signal<Int64?, NoError> { subscriber in
|
||||
if let signal = editingState.priceSignal(forIdentifier: asset.uniqueIdentifier) {
|
||||
let disposable = signal.start(next: { next in
|
||||
subscriber.putNext(next as? Int64)
|
||||
}, error: { _ in
|
||||
}, completed: nil)!
|
||||
|
||||
return ActionDisposable {
|
||||
disposable.dispose()
|
||||
}
|
||||
} else {
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
|
||||
self.spoilerDisposable.set((combineLatest(spoilerSignal, priceSignal)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] hasSpoiler, price in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.updateHasSpoiler(hasSpoiler)
|
||||
strongSelf.updateHasSpoiler(hasSpoiler, price: price)
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -163,14 +178,14 @@ private class MediaPickerSelectedItemNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
private var didSetupSpoiler = false
|
||||
private func updateHasSpoiler(_ hasSpoiler: Bool) {
|
||||
private func updateHasSpoiler(_ hasSpoiler: Bool, price: Int64?) {
|
||||
var animated = true
|
||||
if !self.didSetupSpoiler {
|
||||
animated = false
|
||||
self.didSetupSpoiler = true
|
||||
}
|
||||
|
||||
if hasSpoiler {
|
||||
if hasSpoiler || price != nil {
|
||||
if self.spoilerNode == nil {
|
||||
let spoilerNode = SpoilerOverlayNode(enableAnimations: self.enableAnimations)
|
||||
self.insertSubnode(spoilerNode, aboveSubnode: self.imageNode)
|
||||
|
|
@ -463,6 +478,80 @@ 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 {
|
||||
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
|
||||
|
|
@ -520,6 +609,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?
|
||||
|
|
@ -634,6 +724,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)
|
||||
|
|
@ -675,6 +773,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()
|
||||
|
||||
|
|
@ -695,6 +797,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)
|
||||
|
|
@ -829,6 +938,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 {
|
||||
|
|
@ -857,6 +968,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 {
|
||||
|
|
@ -1047,6 +1162,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
|
||||
|
|
@ -1054,7 +1194,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
|
||||
|
|
|
|||
|
|
@ -976,6 +976,10 @@ public final class StoreMessage {
|
|||
}
|
||||
}
|
||||
|
||||
public func withUpdatedMedia(_ media: [Media]) -> StoreMessage {
|
||||
return StoreMessage(id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, threadId: self.threadId, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, authorId: self.authorId, text: self.text, attributes: self.attributes, media: media)
|
||||
}
|
||||
|
||||
public func withUpdatedAttributes(_ attributes: [MessageAttribute]) -> StoreMessage {
|
||||
return StoreMessage(id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, threadId: self.threadId, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, authorId: self.authorId, text: self.text, attributes: attributes, media: self.media)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1078,6 +1078,26 @@ private final class DemoSheetContent: CombinedComponent {
|
|||
)
|
||||
)
|
||||
|
||||
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] = []
|
||||
if let item = availableItems.first(where: { $0.value.content.id == component.subject as AnyHashable }) {
|
||||
|
|
@ -1172,6 +1192,8 @@ private final class DemoSheetContent: CombinedComponent {
|
|||
text = strings.Premium_MessagePrivacyInfo
|
||||
case .folderTags:
|
||||
text = strings.Premium_FolderTagsStandaloneInfo
|
||||
case .messageEffects:
|
||||
text = strings.Premium_MessageEffectsInfo
|
||||
default:
|
||||
text = ""
|
||||
}
|
||||
|
|
@ -1441,6 +1463,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
|
|||
case messagePrivacy
|
||||
case business
|
||||
case folderTags
|
||||
case messageEffects
|
||||
|
||||
case businessLocation
|
||||
case businessHours
|
||||
|
|
@ -1497,6 +1520,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,25 @@ public class PremiumLimitsListScreen: ViewController {
|
|||
)
|
||||
)
|
||||
)
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
|
||||
"//submodules/Components/SheetComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/MultilineTextWithEntitiesComponent",
|
||||
"//submodules/TelegramNotices",
|
||||
|
|
@ -50,6 +51,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",
|
||||
|
|
|
|||
|
|
@ -242,9 +242,9 @@ private final class BoostsTabsItemNode: ListViewItemNode {
|
|||
|
||||
switch item.selectedTab {
|
||||
case .boosts:
|
||||
selectionFrame = CGRect(x: strongSelf.boostsTextNode.frame.minX, y: layoutSize.height - selectionHeight, width: strongSelf.boostsTextNode.frame.width, height: selectionHeight)
|
||||
selectionFrame = CGRect(x: strongSelf.boostsTextNode.frame.minX, y: contentSize.height - selectionHeight, width: strongSelf.boostsTextNode.frame.width, height: selectionHeight)
|
||||
case .gifts:
|
||||
selectionFrame = CGRect(x: strongSelf.giftsTextNode.frame.minX, y: layoutSize.height - selectionHeight, width: strongSelf.giftsTextNode.frame.width, height: selectionHeight)
|
||||
selectionFrame = CGRect(x: strongSelf.giftsTextNode.frame.minX, y: contentSize.height - selectionHeight, width: strongSelf.giftsTextNode.frame.width, height: selectionHeight)
|
||||
}
|
||||
|
||||
var transition: ContainedViewLayoutTransition = .immediate
|
||||
|
|
|
|||
|
|
@ -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,22 @@ 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 showTimeoutTooltip: (Int32) -> Void
|
||||
let buyAds: () -> Void
|
||||
let openMonetizationIntro: () -> Void
|
||||
let openMonetizationInfo: () -> Void
|
||||
let openTransaction: (RevenueStatsTransactionsContext.State.Transaction) -> Void
|
||||
let expandTransactions: () -> Void
|
||||
let openTonTransaction: (RevenueStatsTransactionsContext.State.Transaction) -> Void
|
||||
let openStarsTransaction: (StarsContext.State.Transaction) -> Void
|
||||
let expandTransactions: (Bool) -> 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, showTimeoutTooltip: @escaping (Int32) -> Void, buyAds: @escaping () -> Void, openMonetizationIntro: @escaping () -> Void, openMonetizationInfo: @escaping () -> Void, openTonTransaction: @escaping (RevenueStatsTransactionsContext.State.Transaction) -> Void, openStarsTransaction: @escaping (StarsContext.State.Transaction) -> Void, expandTransactions: @escaping (Bool) -> Void, updateCpmEnabled: @escaping (Bool) -> Void, presentCpmLocked: @escaping () -> Void, dismissInput: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.loadDetailedGraph = loadDetailedGraph
|
||||
self.openPostStats = openPostStats
|
||||
|
|
@ -65,10 +71,15 @@ 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.showTimeoutTooltip = showTimeoutTooltip
|
||||
self.buyAds = buyAds
|
||||
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 +112,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,19 +231,28 @@ 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, Int32?)
|
||||
case adsStarsBalanceInfo(PresentationTheme, String)
|
||||
|
||||
case adsTransactionsTitle(PresentationTheme, String)
|
||||
case adsTransactionsTabs(PresentationTheme, String, String, Bool)
|
||||
case adsTransaction(Int32, PresentationTheme, RevenueStatsTransactionsContext.State.Transaction)
|
||||
case adsTransactionsExpand(PresentationTheme, String)
|
||||
case adsStarsTransaction(Int32, PresentationTheme, StarsContext.State.Transaction)
|
||||
case adsTransactionsExpand(PresentationTheme, String, Bool)
|
||||
|
||||
case adsCpmToggle(PresentationTheme, String, Int32, Bool?)
|
||||
case adsCpmInfo(PresentationTheme, String)
|
||||
|
|
@ -281,13 +303,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 +418,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 +749,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 +779,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, lhsCooldownUntilTimestamp):
|
||||
if case let .adsStarsBalance(rhsTheme, rhsStats, rhsCanWithdraw, rhsIsEnabled, rhsCooldownUntilTimestamp) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsCanWithdraw == rhsCanWithdraw, lhsIsEnabled == rhsIsEnabled, lhsCooldownUntilTimestamp == rhsCooldownUntilTimestamp {
|
||||
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,14 +827,26 @@ 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 .adsTransactionsExpand(lhsTheme, lhsText):
|
||||
if case let .adsTransactionsExpand(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
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, lhsStars):
|
||||
if case let .adsTransactionsExpand(rhsTheme, rhsText, rhsStars) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsStars == rhsStars {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
|
|
@ -811,9 +893,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 +919,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,25 +1045,64 @@ 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,
|
||||
stats: stats,
|
||||
canWithdraw: canWithdraw,
|
||||
isEnabled: isEnabled,
|
||||
actionCooldownUntilTimestamp: nil,
|
||||
withdrawAction: {
|
||||
arguments.requestWithdraw()
|
||||
arguments.requestTonWithdraw()
|
||||
},
|
||||
buyAdsAction: nil,
|
||||
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, cooldownUntilTimestamp):
|
||||
return MonetizationBalanceItem(
|
||||
context: arguments.context,
|
||||
presentationData: presentationData,
|
||||
stats: stats,
|
||||
canWithdraw: canWithdraw,
|
||||
isEnabled: isEnabled,
|
||||
actionCooldownUntilTimestamp: cooldownUntilTimestamp,
|
||||
withdrawAction: {
|
||||
var remainingCooldownSeconds: Int32 = 0
|
||||
if let cooldownUntilTimestamp {
|
||||
remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970)
|
||||
remainingCooldownSeconds = max(0, remainingCooldownSeconds)
|
||||
|
||||
if remainingCooldownSeconds > 0 {
|
||||
arguments.showTimeoutTooltip(cooldownUntilTimestamp)
|
||||
} else {
|
||||
arguments.requestStarsWithdraw()
|
||||
}
|
||||
} else {
|
||||
arguments.requestStarsWithdraw()
|
||||
}
|
||||
},
|
||||
buyAdsAction: canWithdraw ? {
|
||||
arguments.buyAds()
|
||||
} : nil,
|
||||
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,11 +1149,15 @@ 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 .adsTransactionsExpand(theme, title):
|
||||
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, stars):
|
||||
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: {
|
||||
arguments.expandTransactions()
|
||||
arguments.expandTransactions(stars)
|
||||
})
|
||||
case let .adsCpmToggle(_, title, minLevel, value):
|
||||
var badgeComponent: AnyComponent<Empty>?
|
||||
|
|
@ -1062,23 +1191,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 +1228,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 +1241,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(_ starsSelected: 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 +1512,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 +1527,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 +1555,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, starsData.balances.nextWithdrawalTimestamp))
|
||||
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,9 +1605,40 @@ 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)))
|
||||
entries.append(.adsTransactionsExpand(presentationData.theme, presentationData.strings.Monetization_Transaction_ShowMoreTransactions(moreCount), false))
|
||||
}
|
||||
}
|
||||
|
||||
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), true))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1471,9 +1669,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 +1711,9 @@ private func channelStatsControllerEntries(
|
|||
data: revenueState,
|
||||
boostData: boostData,
|
||||
transactionsInfo: revenueTransactions,
|
||||
starsData: starsState,
|
||||
starsTransactionsInfo: starsTransactions,
|
||||
adsRestricted: adsRestricted,
|
||||
animatedEmojis: animatedEmojis,
|
||||
premiumConfiguration: premiumConfiguration,
|
||||
monetizationConfiguration: monetizationConfiguration
|
||||
)
|
||||
|
|
@ -1523,8 +1723,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 +1783,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 +1798,12 @@ 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 showTimeoutTooltipImpl: ((Int32) -> Void)?
|
||||
var buyAdsImpl: (() -> Void)?
|
||||
var updateStatusBarImpl: ((StatusBarStyle) -> Void)?
|
||||
var dismissInputImpl: (() -> Void)?
|
||||
|
||||
|
|
@ -1716,8 +1926,20 @@ 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?()
|
||||
},
|
||||
showTimeoutTooltip: { timestamp in
|
||||
showTimeoutTooltipImpl?(timestamp)
|
||||
},
|
||||
buyAds: {
|
||||
buyAdsImpl?()
|
||||
},
|
||||
openMonetizationIntro: {
|
||||
let controller = MonetizationIntroScreen(context: context, openMore: {})
|
||||
|
|
@ -1727,10 +1949,13 @@ 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)
|
||||
},
|
||||
expandTransactions: {
|
||||
openStarsTransaction: { transaction in
|
||||
openStarsTransactionImpl?(transaction)
|
||||
},
|
||||
expandTransactions: { stars in
|
||||
updateState { state in
|
||||
if state.transactionsExpanded {
|
||||
return state.withUpdatedMoreTransactionsDisplayed(state.moreTransactionsDisplayed + 50)
|
||||
|
|
@ -1738,7 +1963,11 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
|
|||
return state.withUpdatedTransactionsExpanded(true)
|
||||
}
|
||||
}
|
||||
revenueTransactions.loadMore()
|
||||
if stars {
|
||||
starsTransactions.loadMore()
|
||||
} else {
|
||||
revenueTransactions.loadMore()
|
||||
}
|
||||
},
|
||||
updateCpmEnabled: { value in
|
||||
let _ = context.engine.peers.updateChannelRestrictAdMessages(peerId: peerId, restricted: value).start()
|
||||
|
|
@ -1802,12 +2031,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 +2130,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 +2139,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 +2353,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 +2365,100 @@ 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)
|
||||
}
|
||||
}))
|
||||
}
|
||||
var tooltipScreen: UndoOverlayController?
|
||||
var timer: Foundation.Timer?
|
||||
showTimeoutTooltipImpl = { cooldownUntilTimestamp in
|
||||
let remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970)
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let content: UndoOverlayContent = .universal(
|
||||
animation: "anim_clock",
|
||||
scale: 0.058,
|
||||
colors: [:],
|
||||
title: nil,
|
||||
text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorTimeout(stringForRemainingTime(remainingCooldownSeconds)).string,
|
||||
customUndoText: nil,
|
||||
timeout: nil
|
||||
)
|
||||
let controller = UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in
|
||||
return true
|
||||
})
|
||||
tooltipScreen = controller
|
||||
presentImpl?(controller)
|
||||
|
||||
if remainingCooldownSeconds < 3600 {
|
||||
if timer == nil {
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { _ in
|
||||
if let tooltipScreen {
|
||||
let remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970)
|
||||
let content: UndoOverlayContent = .universal(
|
||||
animation: "anim_clock",
|
||||
scale: 0.058,
|
||||
colors: [:],
|
||||
title: nil,
|
||||
text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorTimeout(stringForRemainingTime(remainingCooldownSeconds)).string,
|
||||
customUndoText: nil,
|
||||
timeout: nil
|
||||
)
|
||||
tooltipScreen.content = content
|
||||
} else {
|
||||
if let currentTimer = timer {
|
||||
timer = nil
|
||||
currentTimer.invalidate()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
buyAdsImpl = {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let _ = (context.engine.peers.requestStarsRevenueAdsAccountlUrl(peerId: peerId)
|
||||
|> deliverOnMainQueue).startStandalone(next: { url in
|
||||
guard let url else {
|
||||
return
|
||||
}
|
||||
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
|
||||
})
|
||||
}
|
||||
openTonTransactionImpl = { transaction in
|
||||
let _ = (peer.get()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
|
|
@ -2146,6 +2470,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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,24 +9,31 @@ import ItemListUI
|
|||
import SolidRoundedButtonNode
|
||||
import TelegramCore
|
||||
import TextFormat
|
||||
import ComponentFlow
|
||||
import ButtonComponent
|
||||
import BundleIconComponent
|
||||
|
||||
final class MonetizationBalanceItem: ListViewItem, ItemListItem {
|
||||
let context: AccountContext
|
||||
let presentationData: ItemListPresentationData
|
||||
let stats: RevenueStats
|
||||
let stats: Stats
|
||||
let canWithdraw: Bool
|
||||
let isEnabled: Bool
|
||||
let actionCooldownUntilTimestamp: Int32?
|
||||
let withdrawAction: () -> Void
|
||||
let buyAdsAction: (() -> Void)?
|
||||
let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
presentationData: ItemListPresentationData,
|
||||
stats: RevenueStats,
|
||||
stats: Stats,
|
||||
canWithdraw: Bool,
|
||||
isEnabled: Bool,
|
||||
actionCooldownUntilTimestamp: Int32?,
|
||||
withdrawAction: @escaping () -> Void,
|
||||
buyAdsAction: (() -> Void)?,
|
||||
sectionId: ItemListSectionId,
|
||||
style: ItemListStyle
|
||||
) {
|
||||
|
|
@ -35,7 +42,9 @@ final class MonetizationBalanceItem: ListViewItem, ItemListItem {
|
|||
self.stats = stats
|
||||
self.canWithdraw = canWithdraw
|
||||
self.isEnabled = isEnabled
|
||||
self.actionCooldownUntilTimestamp = actionCooldownUntilTimestamp
|
||||
self.withdrawAction = withdrawAction
|
||||
self.buyAdsAction = buyAdsAction
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
}
|
||||
|
|
@ -85,12 +94,15 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode {
|
|||
private let iconNode: ASImageNode
|
||||
private let balanceTextNode: TextNode
|
||||
private let valueTextNode: TextNode
|
||||
|
||||
private var withdrawButtonNode: SolidRoundedButtonNode?
|
||||
private var button = ComponentView<Empty>()
|
||||
private var buyButton = ComponentView<Empty>()
|
||||
|
||||
private let activateArea: AccessibilityAreaNode
|
||||
|
||||
private var timer: Foundation.Timer?
|
||||
|
||||
private var item: MonetizationBalanceItem?
|
||||
private var buttonLayout: (isStars: Bool, origin: CGFloat, width: CGFloat, leftInset: CGFloat, rightInset: CGFloat)?
|
||||
|
||||
override var canBeSelected: Bool {
|
||||
return false
|
||||
|
|
@ -158,13 +170,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, divide: false, 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 +295,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()
|
||||
|
|
@ -288,37 +315,131 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode {
|
|||
|
||||
strongSelf.valueTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - valueLayout.size.width) / 2.0), y: balanceTextFrame.maxY - 5.0), size: valueLayout.size)
|
||||
|
||||
if item.canWithdraw {
|
||||
let withdrawButtonNode: SolidRoundedButtonNode
|
||||
if let currentWithdrawButtonNode = strongSelf.withdrawButtonNode {
|
||||
withdrawButtonNode = currentWithdrawButtonNode
|
||||
} else {
|
||||
var buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme)
|
||||
buttonTheme = buttonTheme.withUpdated(disabledBackgroundColor: buttonTheme.backgroundColor, disabledForegroundColor: buttonTheme.foregroundColor.withAlphaComponent(0.6))
|
||||
withdrawButtonNode = SolidRoundedButtonNode(theme: buttonTheme, height: buttonHeight, cornerRadius: 11.0)
|
||||
withdrawButtonNode.pressed = { [weak self] in
|
||||
if let self, let item = self.item, item.isEnabled {
|
||||
item.withdrawAction()
|
||||
}
|
||||
}
|
||||
strongSelf.addSubnode(withdrawButtonNode)
|
||||
strongSelf.withdrawButtonNode = withdrawButtonNode
|
||||
}
|
||||
withdrawButtonNode.title = item.presentationData.strings.Monetization_BalanceWithdraw
|
||||
withdrawButtonNode.isEnabled = item.isEnabled
|
||||
|
||||
let buttonWidth = contentSize.width - leftInset - rightInset
|
||||
let _ = withdrawButtonNode.updateLayout(width: buttonWidth, transition: .immediate)
|
||||
withdrawButtonNode.frame = CGRect(x: leftInset, y: strongSelf.valueTextNode.frame.maxY + buttonSpacing + 3.0, width: buttonWidth, height: buttonHeight)
|
||||
} else {
|
||||
strongSelf.withdrawButtonNode?.removeFromSupernode()
|
||||
strongSelf.withdrawButtonNode = nil
|
||||
}
|
||||
strongSelf.buttonLayout = (isStars: isStars, origin: strongSelf.valueTextNode.frame.maxY + buttonSpacing + 3.0, width: params.width, leftInset: leftInset, rightInset: rightInset)
|
||||
strongSelf.updateButton()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateButton() {
|
||||
guard let item = self.item, let (isStars, origin, width, leftInset, rightInset) = self.buttonLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
if item.canWithdraw {
|
||||
var remainingCooldownSeconds: Int32 = 0
|
||||
if let cooldownUntilTimestamp = item.actionCooldownUntilTimestamp {
|
||||
remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970)
|
||||
remainingCooldownSeconds = max(0, remainingCooldownSeconds)
|
||||
}
|
||||
|
||||
if remainingCooldownSeconds > 0 {
|
||||
if self.timer == nil {
|
||||
self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updateButton()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if let timer = self.timer {
|
||||
self.timer = nil
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
var actionTitle = isStars ? item.presentationData.strings.Monetization_BalanceStarsWithdraw : item.presentationData.strings.Monetization_BalanceWithdraw
|
||||
var withdrawWidth = width - leftInset - rightInset
|
||||
if let _ = item.buyAdsAction {
|
||||
withdrawWidth = (withdrawWidth - 10.0) / 2.0
|
||||
actionTitle = item.presentationData.strings.Monetization_BalanceStarsWithdrawShort
|
||||
}
|
||||
|
||||
let content: AnyComponentWithIdentity<Empty>
|
||||
if remainingCooldownSeconds > 0 {
|
||||
content = AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(
|
||||
VStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(Text(text: actionTitle, font: Font.semibold(17.0), color: item.presentationData.theme.list.itemCheckColors.foregroundColor))),
|
||||
AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(HStack([
|
||||
AnyComponentWithIdentity(id: 1, component: AnyComponent(BundleIconComponent(name: "Chat List/StatusLockIcon", tintColor: item.presentationData.theme.list.itemCheckColors.fillColor.mixedWith(item.presentationData.theme.list.itemCheckColors.foregroundColor, alpha: 0.7)))),
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(Text(text: stringForRemainingTime(remainingCooldownSeconds), font: Font.with(size: 11.0, weight: .medium, traits: [.monospacedNumbers]), color: item.presentationData.theme.list.itemCheckColors.fillColor.mixedWith(item.presentationData.theme.list.itemCheckColors.foregroundColor, alpha: 0.7))))
|
||||
], spacing: 3.0)))
|
||||
], spacing: 1.0)
|
||||
))
|
||||
} else {
|
||||
content = AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: actionTitle, font: Font.semibold(17.0), color: item.presentationData.theme.list.itemCheckColors.foregroundColor)))
|
||||
}
|
||||
|
||||
let buttonSize = self.button.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
color: item.presentationData.theme.list.itemCheckColors.fillColor,
|
||||
foreground: item.presentationData.theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: item.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
|
||||
),
|
||||
content: content,
|
||||
isEnabled: item.isEnabled,
|
||||
allowActionWhenDisabled: false,
|
||||
displaysProgress: false,
|
||||
action: { [weak self] in
|
||||
guard let self, let item = self.item, item.isEnabled else {
|
||||
return
|
||||
}
|
||||
item.withdrawAction()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: withdrawWidth, height: 50.0)
|
||||
)
|
||||
if let buttonView = self.button.view {
|
||||
if buttonView.superview == nil {
|
||||
self.view.addSubview(buttonView)
|
||||
}
|
||||
let buttonFrame = CGRect(origin: CGPoint(x: leftInset, y: origin), size: buttonSize)
|
||||
buttonView.frame = buttonFrame
|
||||
}
|
||||
|
||||
if let _ = item.buyAdsAction {
|
||||
let buyButtonSize = self.buyButton.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
color: item.presentationData.theme.list.itemCheckColors.fillColor,
|
||||
foreground: item.presentationData.theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: item.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
|
||||
),
|
||||
content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: item.presentationData.strings.Monetization_BalanceStarsBuyAds, font: Font.semibold(17.0), color: item.presentationData.theme.list.itemCheckColors.foregroundColor))),
|
||||
isEnabled: true,
|
||||
allowActionWhenDisabled: false,
|
||||
displaysProgress: false,
|
||||
action: { [weak self] in
|
||||
guard let self, let item = self.item, item.isEnabled else {
|
||||
return
|
||||
}
|
||||
item.buyAdsAction?()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: withdrawWidth, height: 50.0)
|
||||
)
|
||||
if let buttonView = self.buyButton.view {
|
||||
if buttonView.superview == nil {
|
||||
self.view.addSubview(buttonView)
|
||||
}
|
||||
let buttonFrame = CGRect(origin: CGPoint(x: leftInset + withdrawWidth + 10.0, y: origin), size: buyButtonSize)
|
||||
buttonView.frame = buttonFrame
|
||||
}
|
||||
} else if let buttonView = self.buyButton.view {
|
||||
buttonView.removeFromSuperview()
|
||||
}
|
||||
} else if let buttonView = self.button.view {
|
||||
buttonView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
|
@ -331,3 +452,16 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode {
|
|||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
||||
func stringForRemainingTime(_ duration: Int32) -> String {
|
||||
let hours = duration / 3600
|
||||
let minutes = duration / 60 % 60
|
||||
let seconds = duration % 60
|
||||
let durationString: String
|
||||
if hours > 0 {
|
||||
durationString = String(format: "%d:%02d", hours, minutes)
|
||||
} else {
|
||||
durationString = String(format: "%02d:%02d", minutes, seconds)
|
||||
}
|
||||
return durationString
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ func formatAddress(_ address: String) -> String {
|
|||
return address
|
||||
}
|
||||
|
||||
func formatUsdValue(_ value: Int64, rate: Double) -> String {
|
||||
let formattedValue = String(format: "%0.2f", (Double(value) / 1000000000) * rate)
|
||||
func formatUsdValue(_ value: Int64, divide: Bool = true, rate: Double) -> String {
|
||||
let normalizedValue: Double = divide ? Double(value) / 1000000000 : Double(value)
|
||||
let formattedValue = String(format: "%0.2f", normalizedValue * rate)
|
||||
return "$\(formattedValue)"
|
||||
}
|
||||
|
||||
|
|
@ -38,6 +39,11 @@ func formatBalanceText(_ value: Int64, decimalSeparator: String, showPlus: Bool
|
|||
} else if showPlus {
|
||||
balanceText.insert("+", at: balanceText.startIndex)
|
||||
}
|
||||
|
||||
if let dec = balanceText.range(of: decimalSeparator) {
|
||||
balanceText = String(balanceText[balanceText.startIndex ..< min(balanceText.endIndex, balanceText.index(dec.upperBound, offsetBy: 2))])
|
||||
}
|
||||
|
||||
return balanceText
|
||||
}
|
||||
|
||||
|
|
|
|||
397
submodules/StatisticsUI/Sources/StarsTransactionItem.swift
Normal file
397
submodules/StatisticsUI/Sources/StarsTransactionItem.swift
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
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 {
|
||||
itemTitle = item.presentationData.strings.Stars_Intro_Transaction_MediaPurchase
|
||||
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 .ads:
|
||||
itemTitle = item.presentationData.strings.Stars_Intro_Transaction_TelegramAds_Title
|
||||
itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_TelegramAds_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: nil, media: [], backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor))), 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)
|
||||
|
|
@ -200,8 +211,26 @@ private final class ValueItemNode: ASDisplayNode {
|
|||
|
||||
let (deltaLayout, deltaApply) = makeDeltaLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: delta?.0 ?? "", font: deltaFont, textColor: deltaColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: constrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
var valueOffset: CGFloat = 0.0
|
||||
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 = 21.0
|
||||
default:
|
||||
iconName = nil
|
||||
}
|
||||
|
||||
let horizontalSpacing: CGFloat = 4.0
|
||||
let size = CGSize(width: valueLayout.size.width + horizontalSpacing + deltaLayout.size.width, height: valueLayout.size.height + titleLayout.size.height)
|
||||
let size = CGSize(
|
||||
width: max(valueOffset + valueLayout.size.width + horizontalSpacing + deltaLayout.size.width, titleLayout.size.width),
|
||||
height: valueLayout.size.height + titleLayout.size.height
|
||||
)
|
||||
return (size, {
|
||||
var themeUpdated = false
|
||||
if targetNode.currentTheme !== presentationData.theme {
|
||||
|
|
@ -212,9 +241,14 @@ private final class ValueItemNode: ASDisplayNode {
|
|||
let _ = valueApply()
|
||||
let _ = titleApply()
|
||||
let _ = deltaApply()
|
||||
|
||||
var iconNameUpdated = false
|
||||
if targetNode.currentIconName != iconName {
|
||||
targetNode.currentIconName = iconName
|
||||
iconNameUpdated = true
|
||||
}
|
||||
|
||||
var valueOffset: CGFloat = 0.0
|
||||
if isTon {
|
||||
if let iconName {
|
||||
let iconNode: ASImageNode
|
||||
if let current = targetNode.iconNode {
|
||||
iconNode = current
|
||||
|
|
@ -225,16 +259,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
|
||||
|
|
@ -352,6 +388,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
}
|
||||
|
||||
var twoColumnLayout = true
|
||||
var useMinLeftColumnWidth = true
|
||||
|
||||
var topLeftItemLayoutAndApply: (CGSize, () -> ValueItemNode)?
|
||||
var topRightItemLayoutAndApply: (CGSize, () -> ValueItemNode)?
|
||||
|
|
@ -382,7 +419,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
compactNumericCountString(stats.views),
|
||||
item.presentationData.strings.Stats_Message_Views,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
topRightItemLayoutAndApply = makeTopRightItemLayout(
|
||||
|
|
@ -392,7 +429,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "–",
|
||||
item.presentationData.strings.Stats_Message_PublicShares,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
|
||||
|
|
@ -402,7 +439,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
compactNumericCountString(stats.reactions),
|
||||
item.presentationData.strings.Stats_Message_Reactions,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
|
||||
|
|
@ -412,7 +449,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 +461,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
compactNumericCountString(views.seenCount),
|
||||
item.presentationData.strings.Stats_Message_Views,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
topRightItemLayoutAndApply = makeTopRightItemLayout(
|
||||
|
|
@ -434,7 +471,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "–",
|
||||
item.presentationData.strings.Stats_Message_PublicShares,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
|
||||
|
|
@ -444,7 +481,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
compactNumericCountString(views.reactedCount),
|
||||
item.presentationData.strings.Stats_Message_Reactions,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
|
||||
|
|
@ -454,7 +491,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 +503,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
"\(stats.level)",
|
||||
item.presentationData.strings.Stats_Boosts_Level,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
var premiumSubscribers: Double = 0.0
|
||||
|
|
@ -481,7 +518,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 +528,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
"\(stats.boosts)",
|
||||
item.presentationData.strings.Stats_Boosts_ExistingBoosts,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
let boostsLeft: Int32
|
||||
|
|
@ -507,7 +544,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
"\(boostsLeft)",
|
||||
item.presentationData.strings.Stats_Boosts_BoostsToLevelUp,
|
||||
nil,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
|
||||
if twoColumnLayout {
|
||||
|
|
@ -532,7 +569,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 +583,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 +642,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
value,
|
||||
title,
|
||||
delta,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
}
|
||||
if let (value, title, delta) = items[1] {
|
||||
|
|
@ -616,7 +653,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
value,
|
||||
title,
|
||||
delta,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
}
|
||||
if let (value, title, delta) = items[2] {
|
||||
|
|
@ -627,7 +664,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
value,
|
||||
title,
|
||||
delta,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
}
|
||||
if let (value, title, delta) = items[3] {
|
||||
|
|
@ -638,7 +675,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
value,
|
||||
title,
|
||||
delta,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
}
|
||||
if let (value, title, delta) = items[4] {
|
||||
|
|
@ -649,7 +686,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
value,
|
||||
title,
|
||||
delta,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
}
|
||||
if let (value, title, delta) = items[5] {
|
||||
|
|
@ -660,7 +697,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
value,
|
||||
title,
|
||||
delta,
|
||||
false
|
||||
.generic
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -684,7 +721,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 +732,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 +743,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 +753,7 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
compactNumericCountString(Int(stats.posters.current)),
|
||||
item.presentationData.strings.Stats_GroupPosters,
|
||||
(postersDelta.text, postersDelta.positive ? .positive : .negative),
|
||||
false
|
||||
.generic
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -726,39 +763,106 @@ 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
|
||||
useMinLeftColumnWidth = true
|
||||
|
||||
topLeftItemLayoutAndApply = makeTopLeftItemLayout(
|
||||
item.context,
|
||||
params.width,
|
||||
item.presentationData,
|
||||
formatBalanceText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator),
|
||||
item.presentationData.strings.Monetization_StarsProceeds_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),
|
||||
item.presentationData.strings.Monetization_StarsProceeds_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),
|
||||
item.presentationData.strings.Monetization_StarsProceeds_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, divide: false, 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, divide: false, 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, divide: false, 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)
|
||||
|
|
@ -858,7 +962,11 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
if let bottomLeftItemLayout = bottomLeftItemLayoutAndApply?.0 {
|
||||
maxLeftWidth = max(maxLeftWidth, bottomLeftItemLayout.width)
|
||||
}
|
||||
secondColumnX = max(layout.size.width / 2.0, firstColumnX + maxLeftWidth + horizontalSpacing)
|
||||
if useMinLeftColumnWidth {
|
||||
secondColumnX = min(layout.size.width / 2.0, firstColumnX + maxLeftWidth + horizontalSpacing * 3.0)
|
||||
} else {
|
||||
secondColumnX = max(layout.size.width / 2.0, firstColumnX + maxLeftWidth + horizontalSpacing)
|
||||
}
|
||||
}
|
||||
|
||||
if let topLeftItemLayout = topLeftItemLayoutAndApply?.0 {
|
||||
|
|
|
|||
|
|
@ -384,6 +384,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[-1759532989] = { return Api.InputMedia.parse_inputMediaGeoLive($0) }
|
||||
dict[-104578748] = { return Api.InputMedia.parse_inputMediaGeoPoint($0) }
|
||||
dict[1080028941] = { return Api.InputMedia.parse_inputMediaInvoice($0) }
|
||||
dict[-1436147773] = { return Api.InputMedia.parse_inputMediaPaidMedia($0) }
|
||||
dict[-1279654347] = { return Api.InputMedia.parse_inputMediaPhoto($0) }
|
||||
dict[-440664550] = { return Api.InputMedia.parse_inputMediaPhotoExternal($0) }
|
||||
dict[261416433] = { return Api.InputMedia.parse_inputMediaPoll($0) }
|
||||
|
|
@ -599,6 +600,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[-626162256] = { return Api.MessageMedia.parse_messageMediaGiveaway($0) }
|
||||
dict[-963047320] = { return Api.MessageMedia.parse_messageMediaGiveawayResults($0) }
|
||||
dict[-156940077] = { return Api.MessageMedia.parse_messageMediaInvoice($0) }
|
||||
dict[-1467669359] = { return Api.MessageMedia.parse_messageMediaPaidMedia($0) }
|
||||
dict[1766936791] = { return Api.MessageMedia.parse_messageMediaPhoto($0) }
|
||||
dict[1272375192] = { return Api.MessageMedia.parse_messageMediaPoll($0) }
|
||||
dict[1758159491] = { return Api.MessageMedia.parse_messageMediaStory($0) }
|
||||
|
|
@ -874,8 +876,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) }
|
||||
dict[2033461574] = { return Api.StarsRevenueStatus.parse_starsRevenueStatus($0) }
|
||||
dict[198776256] = { return Api.StarsTopupOption.parse_starsTopupOption($0) }
|
||||
dict[-1442789224] = { return Api.StarsTransaction.parse_starsTransaction($0) }
|
||||
dict[766853519] = { return Api.StarsTransaction.parse_starsTransaction($0) }
|
||||
dict[-670195363] = { return Api.StarsTransactionPeer.parse_starsTransactionPeer($0) }
|
||||
dict[1617438738] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAds($0) }
|
||||
dict[-1269320843] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAppStore($0) }
|
||||
dict[-382740222] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerFragment($0) }
|
||||
dict[2069236235] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerPlayMarket($0) }
|
||||
|
|
@ -995,7 +998,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[1442983757] = { return Api.Update.parse_updateLangPack($0) }
|
||||
dict[1180041828] = { return Api.Update.parse_updateLangPackTooLong($0) }
|
||||
dict[1448076945] = { return Api.Update.parse_updateLoginToken($0) }
|
||||
dict[1517529484] = { return Api.Update.parse_updateMessageExtendedMedia($0) }
|
||||
dict[-710666460] = { return Api.Update.parse_updateMessageExtendedMedia($0) }
|
||||
dict[1318109142] = { return Api.Update.parse_updateMessageID($0) }
|
||||
dict[-1398708869] = { return Api.Update.parse_updateMessagePoll($0) }
|
||||
dict[619974263] = { return Api.Update.parse_updateMessagePollVote($0) }
|
||||
|
|
@ -1312,6 +1315,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[1314881805] = { return Api.payments.PaymentResult.parse_paymentResult($0) }
|
||||
dict[-666824391] = { return Api.payments.PaymentResult.parse_paymentVerificationNeeded($0) }
|
||||
dict[-74456004] = { return Api.payments.SavedInfo.parse_savedInfo($0) }
|
||||
dict[961445665] = { return Api.payments.StarsRevenueAdsAccountUrl.parse_starsRevenueAdsAccountUrl($0) }
|
||||
dict[-919881925] = { return Api.payments.StarsRevenueStats.parse_starsRevenueStats($0) }
|
||||
dict[497778871] = { return Api.payments.StarsRevenueWithdrawalUrl.parse_starsRevenueWithdrawalUrl($0) }
|
||||
dict[-1930105248] = { return Api.payments.StarsStatus.parse_starsStatus($0) }
|
||||
|
|
@ -2341,6 +2345,8 @@ public extension Api {
|
|||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.payments.SavedInfo:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.payments.StarsRevenueAdsAccountUrl:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.payments.StarsRevenueStats:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.payments.StarsRevenueWithdrawalUrl:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ public extension Api {
|
|||
case inputMediaGeoLive(flags: Int32, geoPoint: Api.InputGeoPoint, heading: Int32?, period: Int32?, proximityNotificationRadius: Int32?)
|
||||
case inputMediaGeoPoint(geoPoint: Api.InputGeoPoint)
|
||||
case inputMediaInvoice(flags: Int32, title: String, description: String, photo: Api.InputWebDocument?, invoice: Api.Invoice, payload: Buffer, provider: String?, providerData: Api.DataJSON, startParam: String?, extendedMedia: Api.InputMedia?)
|
||||
case inputMediaPaidMedia(starsAmount: Int64, extendedMedia: [Api.InputMedia])
|
||||
case inputMediaPhoto(flags: Int32, id: Api.InputPhoto, ttlSeconds: Int32?)
|
||||
case inputMediaPhotoExternal(flags: Int32, url: String, ttlSeconds: Int32?)
|
||||
case inputMediaPoll(flags: Int32, poll: Api.Poll, correctAnswers: [Buffer]?, solution: String?, solutionEntities: [Api.MessageEntity]?)
|
||||
|
|
@ -95,6 +96,17 @@ public extension Api {
|
|||
if Int(flags) & Int(1 << 1) != 0 {serializeString(startParam!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 2) != 0 {extendedMedia!.serialize(buffer, true)}
|
||||
break
|
||||
case .inputMediaPaidMedia(let starsAmount, let extendedMedia):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1436147773)
|
||||
}
|
||||
serializeInt64(starsAmount, buffer: buffer, boxed: false)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(extendedMedia.count))
|
||||
for item in extendedMedia {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
break
|
||||
case .inputMediaPhoto(let flags, let id, let ttlSeconds):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1279654347)
|
||||
|
|
@ -210,6 +222,8 @@ public extension Api {
|
|||
return ("inputMediaGeoPoint", [("geoPoint", geoPoint as Any)])
|
||||
case .inputMediaInvoice(let flags, let title, let description, let photo, let invoice, let payload, let provider, let providerData, let startParam, let extendedMedia):
|
||||
return ("inputMediaInvoice", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("invoice", invoice as Any), ("payload", payload as Any), ("provider", provider as Any), ("providerData", providerData as Any), ("startParam", startParam as Any), ("extendedMedia", extendedMedia as Any)])
|
||||
case .inputMediaPaidMedia(let starsAmount, let extendedMedia):
|
||||
return ("inputMediaPaidMedia", [("starsAmount", starsAmount as Any), ("extendedMedia", extendedMedia as Any)])
|
||||
case .inputMediaPhoto(let flags, let id, let ttlSeconds):
|
||||
return ("inputMediaPhoto", [("flags", flags as Any), ("id", id as Any), ("ttlSeconds", ttlSeconds as Any)])
|
||||
case .inputMediaPhotoExternal(let flags, let url, let ttlSeconds):
|
||||
|
|
@ -399,6 +413,22 @@ public extension Api {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_inputMediaPaidMedia(_ reader: BufferReader) -> InputMedia? {
|
||||
var _1: Int64?
|
||||
_1 = reader.readInt64()
|
||||
var _2: [Api.InputMedia]?
|
||||
if let _ = reader.readInt32() {
|
||||
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputMedia.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
if _c1 && _c2 {
|
||||
return Api.InputMedia.inputMediaPaidMedia(starsAmount: _1!, extendedMedia: _2!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_inputMediaPhoto(_ reader: BufferReader) -> InputMedia? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
|
|
|
|||
|
|
@ -718,6 +718,7 @@ public extension Api {
|
|||
case messageMediaGiveaway(flags: Int32, channels: [Int64], countriesIso2: [String]?, prizeDescription: String?, quantity: Int32, months: Int32, untilDate: Int32)
|
||||
case messageMediaGiveawayResults(flags: Int32, channelId: Int64, additionalPeersCount: Int32?, launchMsgId: Int32, winnersCount: Int32, unclaimedCount: Int32, winners: [Int64], months: Int32, prizeDescription: String?, untilDate: Int32)
|
||||
case messageMediaInvoice(flags: Int32, title: String, description: String, photo: Api.WebDocument?, receiptMsgId: Int32?, currency: String, totalAmount: Int64, startParam: String, extendedMedia: Api.MessageExtendedMedia?)
|
||||
case messageMediaPaidMedia(starsAmount: Int64, extendedMedia: [Api.MessageExtendedMedia])
|
||||
case messageMediaPhoto(flags: Int32, photo: Api.Photo?, ttlSeconds: Int32?)
|
||||
case messageMediaPoll(poll: Api.Poll, results: Api.PollResults)
|
||||
case messageMediaStory(flags: Int32, peer: Api.Peer, id: Int32, story: Api.StoryItem?)
|
||||
|
|
@ -834,6 +835,17 @@ public extension Api {
|
|||
serializeString(startParam, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 4) != 0 {extendedMedia!.serialize(buffer, true)}
|
||||
break
|
||||
case .messageMediaPaidMedia(let starsAmount, let extendedMedia):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1467669359)
|
||||
}
|
||||
serializeInt64(starsAmount, buffer: buffer, boxed: false)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(extendedMedia.count))
|
||||
for item in extendedMedia {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
break
|
||||
case .messageMediaPhoto(let flags, let photo, let ttlSeconds):
|
||||
if boxed {
|
||||
buffer.appendInt32(1766936791)
|
||||
|
|
@ -907,6 +919,8 @@ public extension Api {
|
|||
return ("messageMediaGiveawayResults", [("flags", flags as Any), ("channelId", channelId as Any), ("additionalPeersCount", additionalPeersCount as Any), ("launchMsgId", launchMsgId as Any), ("winnersCount", winnersCount as Any), ("unclaimedCount", unclaimedCount as Any), ("winners", winners as Any), ("months", months as Any), ("prizeDescription", prizeDescription as Any), ("untilDate", untilDate as Any)])
|
||||
case .messageMediaInvoice(let flags, let title, let description, let photo, let receiptMsgId, let currency, let totalAmount, let startParam, let extendedMedia):
|
||||
return ("messageMediaInvoice", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("receiptMsgId", receiptMsgId as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("startParam", startParam as Any), ("extendedMedia", extendedMedia as Any)])
|
||||
case .messageMediaPaidMedia(let starsAmount, let extendedMedia):
|
||||
return ("messageMediaPaidMedia", [("starsAmount", starsAmount as Any), ("extendedMedia", extendedMedia as Any)])
|
||||
case .messageMediaPhoto(let flags, let photo, let ttlSeconds):
|
||||
return ("messageMediaPhoto", [("flags", flags as Any), ("photo", photo as Any), ("ttlSeconds", ttlSeconds as Any)])
|
||||
case .messageMediaPoll(let poll, let results):
|
||||
|
|
@ -1149,6 +1163,22 @@ public extension Api {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_messageMediaPaidMedia(_ reader: BufferReader) -> MessageMedia? {
|
||||
var _1: Int64?
|
||||
_1 = reader.readInt64()
|
||||
var _2: [Api.MessageExtendedMedia]?
|
||||
if let _ = reader.readInt32() {
|
||||
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageExtendedMedia.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
if _c1 && _c2 {
|
||||
return Api.MessageMedia.messageMediaPaidMedia(starsAmount: _1!, extendedMedia: _2!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_messageMediaPhoto(_ reader: BufferReader) -> MessageMedia? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
|
|
|
|||
|
|
@ -708,13 +708,13 @@ public extension Api {
|
|||
}
|
||||
public extension Api {
|
||||
enum StarsTransaction: TypeConstructorDescription {
|
||||
case starsTransaction(flags: Int32, id: String, stars: Int64, date: Int32, peer: Api.StarsTransactionPeer, title: String?, description: String?, photo: Api.WebDocument?, transactionDate: Int32?, transactionUrl: String?)
|
||||
case starsTransaction(flags: Int32, id: String, stars: Int64, date: Int32, peer: Api.StarsTransactionPeer, title: String?, description: String?, photo: Api.WebDocument?, transactionDate: Int32?, transactionUrl: String?, botPayload: Buffer?, msgId: Int32?, extendedMedia: [Api.MessageMedia]?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl):
|
||||
case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1442789224)
|
||||
buffer.appendInt32(766853519)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeString(id, buffer: buffer, boxed: false)
|
||||
|
|
@ -726,14 +726,21 @@ public extension Api {
|
|||
if Int(flags) & Int(1 << 2) != 0 {photo!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 5) != 0 {serializeInt32(transactionDate!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 5) != 0 {serializeString(transactionUrl!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 7) != 0 {serializeBytes(botPayload!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 8) != 0 {serializeInt32(msgId!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 9) != 0 {buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(extendedMedia!.count))
|
||||
for item in extendedMedia! {
|
||||
item.serialize(buffer, true)
|
||||
}}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl):
|
||||
return ("starsTransaction", [("flags", flags as Any), ("id", id as Any), ("stars", stars as Any), ("date", date as Any), ("peer", peer as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("transactionDate", transactionDate as Any), ("transactionUrl", transactionUrl as Any)])
|
||||
case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia):
|
||||
return ("starsTransaction", [("flags", flags as Any), ("id", id as Any), ("stars", stars as Any), ("date", date as Any), ("peer", peer as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("transactionDate", transactionDate as Any), ("transactionUrl", transactionUrl as Any), ("botPayload", botPayload as Any), ("msgId", msgId as Any), ("extendedMedia", extendedMedia as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -762,6 +769,14 @@ public extension Api {
|
|||
if Int(_1!) & Int(1 << 5) != 0 {_9 = reader.readInt32() }
|
||||
var _10: String?
|
||||
if Int(_1!) & Int(1 << 5) != 0 {_10 = parseString(reader) }
|
||||
var _11: Buffer?
|
||||
if Int(_1!) & Int(1 << 7) != 0 {_11 = parseBytes(reader) }
|
||||
var _12: Int32?
|
||||
if Int(_1!) & Int(1 << 8) != 0 {_12 = reader.readInt32() }
|
||||
var _13: [Api.MessageMedia]?
|
||||
if Int(_1!) & Int(1 << 9) != 0 {if let _ = reader.readInt32() {
|
||||
_13 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageMedia.self)
|
||||
} }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
|
|
@ -772,8 +787,11 @@ public extension Api {
|
|||
let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil
|
||||
let _c9 = (Int(_1!) & Int(1 << 5) == 0) || _9 != nil
|
||||
let _c10 = (Int(_1!) & Int(1 << 5) == 0) || _10 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 {
|
||||
return Api.StarsTransaction.starsTransaction(flags: _1!, id: _2!, stars: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8, transactionDate: _9, transactionUrl: _10)
|
||||
let _c11 = (Int(_1!) & Int(1 << 7) == 0) || _11 != nil
|
||||
let _c12 = (Int(_1!) & Int(1 << 8) == 0) || _12 != nil
|
||||
let _c13 = (Int(_1!) & Int(1 << 9) == 0) || _13 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 {
|
||||
return Api.StarsTransaction.starsTransaction(flags: _1!, id: _2!, stars: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8, transactionDate: _9, transactionUrl: _10, botPayload: _11, msgId: _12, extendedMedia: _13)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
|
|
@ -785,6 +803,7 @@ public extension Api {
|
|||
public extension Api {
|
||||
enum StarsTransactionPeer: TypeConstructorDescription {
|
||||
case starsTransactionPeer(peer: Api.Peer)
|
||||
case starsTransactionPeerAds
|
||||
case starsTransactionPeerAppStore
|
||||
case starsTransactionPeerFragment
|
||||
case starsTransactionPeerPlayMarket
|
||||
|
|
@ -798,6 +817,12 @@ public extension Api {
|
|||
buffer.appendInt32(-670195363)
|
||||
}
|
||||
peer.serialize(buffer, true)
|
||||
break
|
||||
case .starsTransactionPeerAds:
|
||||
if boxed {
|
||||
buffer.appendInt32(1617438738)
|
||||
}
|
||||
|
||||
break
|
||||
case .starsTransactionPeerAppStore:
|
||||
if boxed {
|
||||
|
|
@ -836,6 +861,8 @@ public extension Api {
|
|||
switch self {
|
||||
case .starsTransactionPeer(let peer):
|
||||
return ("starsTransactionPeer", [("peer", peer as Any)])
|
||||
case .starsTransactionPeerAds:
|
||||
return ("starsTransactionPeerAds", [])
|
||||
case .starsTransactionPeerAppStore:
|
||||
return ("starsTransactionPeerAppStore", [])
|
||||
case .starsTransactionPeerFragment:
|
||||
|
|
@ -862,6 +889,9 @@ public extension Api {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_starsTransactionPeerAds(_ reader: BufferReader) -> StarsTransactionPeer? {
|
||||
return Api.StarsTransactionPeer.starsTransactionPeerAds
|
||||
}
|
||||
public static func parse_starsTransactionPeerAppStore(_ reader: BufferReader) -> StarsTransactionPeer? {
|
||||
return Api.StarsTransactionPeer.starsTransactionPeerAppStore
|
||||
}
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@ public extension Api {
|
|||
case updateLangPack(difference: Api.LangPackDifference)
|
||||
case updateLangPackTooLong(langCode: String)
|
||||
case updateLoginToken
|
||||
case updateMessageExtendedMedia(peer: Api.Peer, msgId: Int32, extendedMedia: Api.MessageExtendedMedia)
|
||||
case updateMessageExtendedMedia(peer: Api.Peer, msgId: Int32, extendedMedia: [Api.MessageExtendedMedia])
|
||||
case updateMessageID(id: Int32, randomId: Int64)
|
||||
case updateMessagePoll(flags: Int32, pollId: Int64, poll: Api.Poll?, results: Api.PollResults)
|
||||
case updateMessagePollVote(pollId: Int64, peer: Api.Peer, options: [Buffer], qts: Int32)
|
||||
|
|
@ -1039,11 +1039,15 @@ public extension Api {
|
|||
break
|
||||
case .updateMessageExtendedMedia(let peer, let msgId, let extendedMedia):
|
||||
if boxed {
|
||||
buffer.appendInt32(1517529484)
|
||||
buffer.appendInt32(-710666460)
|
||||
}
|
||||
peer.serialize(buffer, true)
|
||||
serializeInt32(msgId, buffer: buffer, boxed: false)
|
||||
extendedMedia.serialize(buffer, true)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(extendedMedia.count))
|
||||
for item in extendedMedia {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
break
|
||||
case .updateMessageID(let id, let randomId):
|
||||
if boxed {
|
||||
|
|
@ -3241,9 +3245,9 @@ public extension Api {
|
|||
}
|
||||
var _2: Int32?
|
||||
_2 = reader.readInt32()
|
||||
var _3: Api.MessageExtendedMedia?
|
||||
if let signature = reader.readInt32() {
|
||||
_3 = Api.parse(reader, signature: signature) as? Api.MessageExtendedMedia
|
||||
var _3: [Api.MessageExtendedMedia]?
|
||||
if let _ = reader.readInt32() {
|
||||
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageExtendedMedia.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
|
|
|
|||
|
|
@ -1044,6 +1044,42 @@ public extension Api.payments {
|
|||
|
||||
}
|
||||
}
|
||||
public extension Api.payments {
|
||||
enum StarsRevenueAdsAccountUrl: TypeConstructorDescription {
|
||||
case starsRevenueAdsAccountUrl(url: String)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .starsRevenueAdsAccountUrl(let url):
|
||||
if boxed {
|
||||
buffer.appendInt32(961445665)
|
||||
}
|
||||
serializeString(url, buffer: buffer, boxed: false)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .starsRevenueAdsAccountUrl(let url):
|
||||
return ("starsRevenueAdsAccountUrl", [("url", url as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_starsRevenueAdsAccountUrl(_ reader: BufferReader) -> StarsRevenueAdsAccountUrl? {
|
||||
var _1: String?
|
||||
_1 = parseString(reader)
|
||||
let _c1 = _1 != nil
|
||||
if _c1 {
|
||||
return Api.payments.StarsRevenueAdsAccountUrl.starsRevenueAdsAccountUrl(url: _1!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api.payments {
|
||||
enum StarsRevenueStats: TypeConstructorDescription {
|
||||
case starsRevenueStats(revenueGraph: Api.StatsGraph, status: Api.StarsRevenueStatus, usdRate: Double)
|
||||
|
|
@ -1626,51 +1662,3 @@ public extension Api.phone {
|
|||
|
||||
}
|
||||
}
|
||||
public extension Api.photos {
|
||||
enum Photo: TypeConstructorDescription {
|
||||
case photo(photo: Api.Photo, users: [Api.User])
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .photo(let photo, let users):
|
||||
if boxed {
|
||||
buffer.appendInt32(539045032)
|
||||
}
|
||||
photo.serialize(buffer, true)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(users.count))
|
||||
for item in users {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .photo(let photo, let users):
|
||||
return ("photo", [("photo", photo as Any), ("users", users as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_photo(_ reader: BufferReader) -> Photo? {
|
||||
var _1: Api.Photo?
|
||||
if let signature = reader.readInt32() {
|
||||
_1 = Api.parse(reader, signature: signature) as? Api.Photo
|
||||
}
|
||||
var _2: [Api.User]?
|
||||
if let _ = reader.readInt32() {
|
||||
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
if _c1 && _c2 {
|
||||
return Api.photos.Photo.photo(photo: _1!, users: _2!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,51 @@
|
|||
public extension Api.photos {
|
||||
enum Photo: TypeConstructorDescription {
|
||||
case photo(photo: Api.Photo, users: [Api.User])
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .photo(let photo, let users):
|
||||
if boxed {
|
||||
buffer.appendInt32(539045032)
|
||||
}
|
||||
photo.serialize(buffer, true)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(users.count))
|
||||
for item in users {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .photo(let photo, let users):
|
||||
return ("photo", [("photo", photo as Any), ("users", users as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_photo(_ reader: BufferReader) -> Photo? {
|
||||
var _1: Api.Photo?
|
||||
if let signature = reader.readInt32() {
|
||||
_1 = Api.parse(reader, signature: signature) as? Api.Photo
|
||||
}
|
||||
var _2: [Api.User]?
|
||||
if let _ = reader.readInt32() {
|
||||
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
if _c1 && _c2 {
|
||||
return Api.photos.Photo.photo(photo: _1!, users: _2!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api.photos {
|
||||
enum Photos: TypeConstructorDescription {
|
||||
case photos(photos: [Api.Photo], users: [Api.User])
|
||||
|
|
@ -1516,77 +1564,3 @@ public extension Api.stories {
|
|||
|
||||
}
|
||||
}
|
||||
public extension Api.stories {
|
||||
enum StoryReactionsList: TypeConstructorDescription {
|
||||
case storyReactionsList(flags: Int32, count: Int32, reactions: [Api.StoryReaction], chats: [Api.Chat], users: [Api.User], nextOffset: String?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .storyReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1436583780)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeInt32(count, buffer: buffer, boxed: false)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(reactions.count))
|
||||
for item in reactions {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(chats.count))
|
||||
for item in chats {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(users.count))
|
||||
for item in users {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .storyReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset):
|
||||
return ("storyReactionsList", [("flags", flags as Any), ("count", count as Any), ("reactions", reactions as Any), ("chats", chats as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_storyReactionsList(_ reader: BufferReader) -> StoryReactionsList? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Int32?
|
||||
_2 = reader.readInt32()
|
||||
var _3: [Api.StoryReaction]?
|
||||
if let _ = reader.readInt32() {
|
||||
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryReaction.self)
|
||||
}
|
||||
var _4: [Api.Chat]?
|
||||
if let _ = reader.readInt32() {
|
||||
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
|
||||
}
|
||||
var _5: [Api.User]?
|
||||
if let _ = reader.readInt32() {
|
||||
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
|
||||
}
|
||||
var _6: String?
|
||||
if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c4 = _4 != nil
|
||||
let _c5 = _5 != nil
|
||||
let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
|
||||
return Api.stories.StoryReactionsList.storyReactionsList(flags: _1!, count: _2!, reactions: _3!, chats: _4!, users: _5!, nextOffset: _6)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,77 @@
|
|||
public extension Api.stories {
|
||||
enum StoryReactionsList: TypeConstructorDescription {
|
||||
case storyReactionsList(flags: Int32, count: Int32, reactions: [Api.StoryReaction], chats: [Api.Chat], users: [Api.User], nextOffset: String?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .storyReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1436583780)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeInt32(count, buffer: buffer, boxed: false)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(reactions.count))
|
||||
for item in reactions {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(chats.count))
|
||||
for item in chats {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(users.count))
|
||||
for item in users {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .storyReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset):
|
||||
return ("storyReactionsList", [("flags", flags as Any), ("count", count as Any), ("reactions", reactions as Any), ("chats", chats as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_storyReactionsList(_ reader: BufferReader) -> StoryReactionsList? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Int32?
|
||||
_2 = reader.readInt32()
|
||||
var _3: [Api.StoryReaction]?
|
||||
if let _ = reader.readInt32() {
|
||||
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryReaction.self)
|
||||
}
|
||||
var _4: [Api.Chat]?
|
||||
if let _ = reader.readInt32() {
|
||||
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
|
||||
}
|
||||
var _5: [Api.User]?
|
||||
if let _ = reader.readInt32() {
|
||||
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
|
||||
}
|
||||
var _6: String?
|
||||
if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c4 = _4 != nil
|
||||
let _c5 = _5 != nil
|
||||
let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
|
||||
return Api.stories.StoryReactionsList.storyReactionsList(flags: _1!, count: _2!, reactions: _3!, chats: _4!, users: _5!, nextOffset: _6)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api.stories {
|
||||
enum StoryViews: TypeConstructorDescription {
|
||||
case storyViews(views: [Api.StoryViews], users: [Api.User])
|
||||
|
|
|
|||
|
|
@ -8731,6 +8731,21 @@ public extension Api.functions.payments {
|
|||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.payments {
|
||||
static func getStarsRevenueAdsAccountUrl(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.StarsRevenueAdsAccountUrl>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-774377531)
|
||||
peer.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "payments.getStarsRevenueAdsAccountUrl", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.StarsRevenueAdsAccountUrl? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.payments.StarsRevenueAdsAccountUrl?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.payments.StarsRevenueAdsAccountUrl
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.payments {
|
||||
static func getStarsRevenueStats(flags: Int32, peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.StarsRevenueStats>) {
|
||||
let buffer = Buffer()
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ enum AccountStateMutationOperation {
|
|||
case UpdateAttachMenuBots
|
||||
case UpdateAudioTranscription(messageId: MessageId, id: Int64, isPending: Bool, text: String)
|
||||
case UpdateConfig
|
||||
case UpdateExtendedMedia(MessageId, Api.MessageExtendedMedia)
|
||||
case UpdateExtendedMedia(MessageId, [Api.MessageExtendedMedia])
|
||||
case ResetForumTopic(topicId: MessageId, data: StoreMessageHistoryThreadData, pts: Int32)
|
||||
case UpdateStory(peerId: PeerId, story: Api.StoryItem)
|
||||
case UpdateReadStories(peerId: PeerId, maxId: Int32)
|
||||
|
|
@ -652,7 +652,7 @@ struct AccountMutableState {
|
|||
self.addOperation(.UpdateConfig)
|
||||
}
|
||||
|
||||
mutating func updateExtendedMedia(_ messageId: MessageId, extendedMedia: Api.MessageExtendedMedia) {
|
||||
mutating func updateExtendedMedia(_ messageId: MessageId, extendedMedia: [Api.MessageExtendedMedia]) {
|
||||
self.addOperation(.UpdateExtendedMedia(messageId, extendedMedia))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -226,6 +226,7 @@ private var declaredEncodables: Void = {
|
|||
declareEncodable(OutgoingQuickReplyMessageAttribute.self, f: { OutgoingQuickReplyMessageAttribute(decoder: $0) })
|
||||
declareEncodable(EffectMessageAttribute.self, f: { EffectMessageAttribute(decoder: $0) })
|
||||
declareEncodable(FactCheckMessageAttribute.self, f: { FactCheckMessageAttribute(decoder: $0) })
|
||||
declareEncodable(TelegramMediaPaidContent.self, f: { TelegramMediaPaidContent(decoder: $0) })
|
||||
return
|
||||
}()
|
||||
|
||||
|
|
|
|||
|
|
@ -391,33 +391,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
|
|||
if (flags & (1 << 1)) != 0 {
|
||||
parsedFlags.insert(.shippingAddressRequested)
|
||||
}
|
||||
|
||||
let extendedMedia: TelegramExtendedMedia?
|
||||
if let apiExtendedMedia = apiExtendedMedia {
|
||||
switch apiExtendedMedia {
|
||||
case let .messageExtendedMediaPreview(_, width, height, thumb, videoDuration):
|
||||
var dimensions: PixelDimensions?
|
||||
if let width = width, let height = height {
|
||||
dimensions = PixelDimensions(width: width, height: height)
|
||||
}
|
||||
var immediateThumbnailData: Data?
|
||||
if let thumb = thumb, case let .photoStrippedSize(_, bytes) = thumb {
|
||||
immediateThumbnailData = bytes.makeData()
|
||||
}
|
||||
extendedMedia = .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: videoDuration)
|
||||
case let .messageExtendedMedia(apiMedia):
|
||||
let (media, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, peerId)
|
||||
if let media = media {
|
||||
extendedMedia = .full(media: media)
|
||||
} else {
|
||||
extendedMedia = nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
extendedMedia = nil
|
||||
}
|
||||
|
||||
return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, extendedMedia: extendedMedia, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), nil, nil, nil, nil)
|
||||
return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, extendedMedia: apiExtendedMedia.flatMap({ TelegramExtendedMedia(apiExtendedMedia: $0, peerId: peerId) }), flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), nil, nil, nil, nil)
|
||||
case let .messageMediaPoll(poll, results):
|
||||
switch poll {
|
||||
case let .poll(id, flags, question, answers, closePeriod, _):
|
||||
|
|
@ -464,6 +438,8 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
|
|||
flags.insert(.refunded)
|
||||
}
|
||||
return (TelegramMediaGiveawayResults(flags: flags, launchMessageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: launchMsgId), additionalChannelsCount: additionalPeersCount ?? 0, winnersPeerIds: winners.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }, winnersCount: winnersCount, unclaimedCount: unclaimedCount, months: months, untilDate: untilDate, prizeDescription: prizeDescription), nil, nil, nil, nil)
|
||||
case let .messageMediaPaidMedia(starsAmount, apiExtendedMedia):
|
||||
return (TelegramMediaPaidContent(amount: starsAmount, extendedMedia: apiExtendedMedia.compactMap({ TelegramExtendedMedia(apiExtendedMedia: $0, peerId: peerId) })), nil, nil, nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import Foundation
|
||||
import Postbox
|
||||
import TelegramApi
|
||||
|
||||
extension TelegramExtendedMedia {
|
||||
init?(apiExtendedMedia: Api.MessageExtendedMedia, peerId: PeerId) {
|
||||
switch apiExtendedMedia {
|
||||
case let .messageExtendedMediaPreview(_, width, height, thumb, videoDuration):
|
||||
var dimensions: PixelDimensions?
|
||||
if let width = width, let height = height {
|
||||
dimensions = PixelDimensions(width: width, height: height)
|
||||
}
|
||||
var immediateThumbnailData: Data?
|
||||
if let thumb = thumb, case let .photoStrippedSize(_, bytes) = thumb {
|
||||
immediateThumbnailData = bytes.makeData()
|
||||
}
|
||||
self = .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: videoDuration)
|
||||
case let .messageExtendedMedia(apiMedia):
|
||||
let (media, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, peerId)
|
||||
if let media = media {
|
||||
self = .full(media: media)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -153,7 +153,13 @@ private func areResourcesEqual(_ lhs: MediaResource, _ rhs: MediaResource) -> Bo
|
|||
}
|
||||
|
||||
private func findMediaResource(media: Media, previousMedia: Media?, resource: MediaResource) -> TelegramMediaResource? {
|
||||
if let image = media as? TelegramMediaImage {
|
||||
if let paidContent = media as? TelegramMediaPaidContent {
|
||||
for case let .full(fullMedia) in paidContent.extendedMedia {
|
||||
if let resource = findMediaResource(media: fullMedia, previousMedia: previousMedia, resource: resource) {
|
||||
return resource
|
||||
}
|
||||
}
|
||||
} else if let image = media as? TelegramMediaImage {
|
||||
for representation in image.representations {
|
||||
if let updatedResource = representation.resource as? CloudPhotoSizeMediaResource, let previousResource = resource as? CloudPhotoSizeMediaResource {
|
||||
if updatedResource.photoId == previousResource.photoId && updatedResource.sizeSpec == previousResource.sizeSpec {
|
||||
|
|
|
|||
|
|
@ -23,8 +23,18 @@ struct PendingMessageUploadedContentAndReuploadInfo {
|
|||
let cacheReferenceKey: CachedSentMediaReferenceKey?
|
||||
}
|
||||
|
||||
struct PendingMessageUploadedContentProgress {
|
||||
let progress: Float
|
||||
let mediaProgress: [MediaId: Float]
|
||||
|
||||
init(progress: Float, mediaProgress: [MediaId: Float] = [:]) {
|
||||
self.progress = progress
|
||||
self.mediaProgress = mediaProgress
|
||||
}
|
||||
}
|
||||
|
||||
enum PendingMessageUploadedContentResult {
|
||||
case progress(Float)
|
||||
case progress(PendingMessageUploadedContentProgress)
|
||||
case content(PendingMessageUploadedContentAndReuploadInfo)
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +100,7 @@ func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Po
|
|||
} else if let media = media.first as? TelegramMediaStory {
|
||||
return .signal(postbox.transaction { transaction -> PendingMessageUploadedContentResult in
|
||||
guard let inputPeer = transaction.getPeer(media.storyId.peerId).flatMap(apiInputPeer) else {
|
||||
return .progress(0.0)
|
||||
return .progress(PendingMessageUploadedContentProgress(progress: 0.0))
|
||||
}
|
||||
return .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaStory(peer: inputPeer, id: media.storyId.id), ""), reuploadInfo: nil, cacheReferenceKey: nil))
|
||||
}
|
||||
|
|
@ -119,6 +129,53 @@ func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Po
|
|||
}
|
||||
|
||||
func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, forceNoBigParts: Bool, peerId: PeerId, media: Media, text: String, autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute?, autoclearMessageAttribute: AutoclearTimeoutMessageAttribute?, messageId: MessageId?, attributes: [MessageAttribute], mediaReference: AnyMediaReference?) -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError>? {
|
||||
if let paidContent = media as? TelegramMediaPaidContent {
|
||||
var signals: [Signal<PendingMessageUploadedContentResult, PendingMessageUploadError>] = []
|
||||
var mediaIds: [MediaId] = []
|
||||
let isGrouped = paidContent.extendedMedia.count > 1
|
||||
for case let .full(media) in paidContent.extendedMedia {
|
||||
guard let id = media.id else {
|
||||
continue
|
||||
}
|
||||
mediaIds.append(id)
|
||||
if let image = media as? TelegramMediaImage {
|
||||
signals.append(uploadedMediaImageContent(network: network, postbox: postbox, transformOutgoingMessageMedia: transformOutgoingMessageMedia, forceReupload: forceReupload, isGrouped: isGrouped, peerId: peerId, image: image, messageId: messageId, text: "", attributes: [], autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, auxiliaryMethods: auxiliaryMethods))
|
||||
} else if let file = media as? TelegramMediaFile {
|
||||
signals.append(uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: forceReupload, isGrouped: isGrouped, isPaid: true, passFetchProgress: false, forceNoBigParts: false, peerId: peerId, messageId: messageId, text: "", attributes: [], autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, file: file))
|
||||
}
|
||||
}
|
||||
return combineLatest(signals)
|
||||
|> map { results -> PendingMessageUploadedContentResult in
|
||||
var currentProgress: Float = 0.0
|
||||
var media: [Api.InputMedia] = []
|
||||
var mediaProgress: [MediaId: Float] = [:]
|
||||
for (mediaId, result) in zip(mediaIds, results) {
|
||||
switch result {
|
||||
case let .progress(progress):
|
||||
currentProgress += progress.progress
|
||||
mediaProgress[mediaId] = progress.progress
|
||||
case let .content(content):
|
||||
if case let .media(resultMedia, _) = content.content {
|
||||
media.append(resultMedia)
|
||||
mediaProgress[mediaId] = 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
let normalizedProgress = currentProgress / Float(results.count)
|
||||
if media.count == results.count {
|
||||
return .content(PendingMessageUploadedContentAndReuploadInfo(
|
||||
content: .media(.inputMediaPaidMedia(
|
||||
starsAmount: paidContent.amount,
|
||||
extendedMedia: media
|
||||
), text),
|
||||
reuploadInfo: nil,
|
||||
cacheReferenceKey: nil
|
||||
))
|
||||
} else {
|
||||
return .progress(PendingMessageUploadedContentProgress(progress: normalizedProgress, mediaProgress: mediaProgress))
|
||||
}
|
||||
}
|
||||
}
|
||||
if let image = media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
|
||||
if peerId.namespace == Namespaces.Peer.SecretChat, let resource = largest.resource as? SecretFileMediaResource {
|
||||
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .secretMedia(.inputEncryptedFile(id: resource.fileId, accessHash: resource.accessHash), resource.decryptedSize, resource.key), reuploadInfo: nil, cacheReferenceKey: nil)))
|
||||
|
|
@ -138,7 +195,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post
|
|||
}
|
||||
}
|
||||
}
|
||||
return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: true, isGrouped: isGrouped, passFetchProgress: false, forceNoBigParts: false, peerId: peerId, messageId: messageId, text: text, attributes: attributes, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, file: file)
|
||||
return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: true, isGrouped: isGrouped, isPaid: false, passFetchProgress: false, forceNoBigParts: false, peerId: peerId, messageId: messageId, text: text, attributes: attributes, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, file: file)
|
||||
} else {
|
||||
if forceReupload {
|
||||
let finalMediaReference: Signal<AnyMediaReference, NoError>
|
||||
|
|
@ -190,7 +247,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post
|
|||
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: emojiSearchQuery), text), reuploadInfo: nil, cacheReferenceKey: nil)))
|
||||
}
|
||||
} else {
|
||||
return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: passFetchProgress, forceNoBigParts: forceNoBigParts, peerId: peerId, messageId: messageId, text: text, attributes: attributes, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, file: file)
|
||||
return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: forceReupload, isGrouped: isGrouped, isPaid: false, passFetchProgress: passFetchProgress, forceNoBigParts: forceNoBigParts, peerId: peerId, messageId: messageId, text: text, attributes: attributes, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, file: file)
|
||||
}
|
||||
} else if let contact = media as? TelegramMediaContact {
|
||||
let input = Api.InputMedia.inputMediaContact(phoneNumber: contact.phoneNumber, firstName: contact.firstName, lastName: contact.lastName, vcard: contact.vCardData ?? "")
|
||||
|
|
@ -426,7 +483,7 @@ if "".isEmpty {
|
|||
flags |= 1 << 1
|
||||
}
|
||||
}
|
||||
return .single(.progress(1.0))
|
||||
return .single(.progress(PendingMessageUploadedContentProgress(progress: 1.0)))
|
||||
|> then(
|
||||
.single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaPhoto(flags: flags, id: .inputPhoto(id: id, accessHash: accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: ttlSeconds), text), reuploadInfo: nil, cacheReferenceKey: nil)))
|
||||
)
|
||||
|
|
@ -462,13 +519,43 @@ if "".isEmpty {
|
|||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: nil, psaType: nil, flags: [])
|
||||
}
|
||||
var updatedAttributes = currentMessage.attributes
|
||||
if let index = updatedAttributes.firstIndex(where: { $0 is OutgoingMessageInfoAttribute }){
|
||||
let attribute = updatedAttributes[index] as! OutgoingMessageInfoAttribute
|
||||
updatedAttributes[index] = attribute.withUpdatedFlags(attribute.flags.union([.transformedMedia]))
|
||||
} else {
|
||||
updatedAttributes.append(OutgoingMessageInfoAttribute(uniqueId: Int64.random(in: Int64.min ... Int64.max), flags: [.transformedMedia], acknowledged: false, correlationId: nil, bubbleUpEmojiOrStickersets: []))
|
||||
|
||||
var markTransformedMedia = true
|
||||
var updatedMedia = currentMessage.media
|
||||
if let paidContent = updatedMedia.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent {
|
||||
var extendedMedia = paidContent.extendedMedia
|
||||
if let index = extendedMedia.firstIndex(where: { media in
|
||||
if case let .full(fullMedia) = media, fullMedia.id == id {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
extendedMedia[index] = .full(media: media)
|
||||
}
|
||||
updatedMedia = [TelegramMediaPaidContent(amount: paidContent.amount, extendedMedia: extendedMedia)]
|
||||
|
||||
if extendedMedia.contains(where: { media in
|
||||
if case .preview = media {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
markTransformedMedia = false
|
||||
}
|
||||
}
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: currentMessage.media))
|
||||
|
||||
if markTransformedMedia {
|
||||
if let index = updatedAttributes.firstIndex(where: { $0 is OutgoingMessageInfoAttribute }){
|
||||
let attribute = updatedAttributes[index] as! OutgoingMessageInfoAttribute
|
||||
updatedAttributes[index] = attribute.withUpdatedFlags(attribute.flags.union([.transformedMedia]))
|
||||
} else {
|
||||
updatedAttributes.append(OutgoingMessageInfoAttribute(uniqueId: Int64.random(in: Int64.min ... Int64.max), flags: [.transformedMedia], acknowledged: false, correlationId: nil, bubbleUpEmojiOrStickersets: []))
|
||||
}
|
||||
}
|
||||
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: updatedMedia))
|
||||
})
|
||||
}
|
||||
return .done(media)
|
||||
|
|
@ -488,7 +575,7 @@ if "".isEmpty {
|
|||
|> mapToSignal { transformResult -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> in
|
||||
switch transformResult {
|
||||
case .pending:
|
||||
return .single(.progress(0.0))
|
||||
return .single(.progress(PendingMessageUploadedContentProgress(progress: 0.0)))
|
||||
case let .done(transformedMedia):
|
||||
let transformedImage = (transformedMedia as? TelegramMediaImage) ?? image
|
||||
guard let largestRepresentation = largestImageRepresentation(transformedImage.representations) else {
|
||||
|
|
@ -505,7 +592,7 @@ if "".isEmpty {
|
|||
|> mapToSignal { next -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> in
|
||||
switch next {
|
||||
case let .progress(progress):
|
||||
return .single(.progress(progress))
|
||||
return .single(.progress(PendingMessageUploadedContentProgress(progress: progress)))
|
||||
case let .inputFile(file):
|
||||
var flags: Int32 = 0
|
||||
var ttlSeconds: Int32?
|
||||
|
|
@ -712,7 +799,7 @@ public func statsCategoryForFileWithAttributes(_ attributes: [TelegramMediaFileA
|
|||
return .file
|
||||
}
|
||||
|
||||
private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, forceNoBigParts: Bool, peerId: PeerId, messageId: MessageId?, text: String, attributes: [MessageAttribute], autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute?, autoclearMessageAttribute: AutoclearTimeoutMessageAttribute?, file: TelegramMediaFile) -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> {
|
||||
private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, forceReupload: Bool, isGrouped: Bool, isPaid: Bool, passFetchProgress: Bool, forceNoBigParts: Bool, peerId: PeerId, messageId: MessageId?, text: String, attributes: [MessageAttribute], autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute?, autoclearMessageAttribute: AutoclearTimeoutMessageAttribute?, file: TelegramMediaFile) -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> {
|
||||
return maybePredownloadedFileResource(postbox: postbox, auxiliaryMethods: auxiliaryMethods, peerId: peerId, resource: file.resource, autoRemove: autoremoveMessageAttribute != nil || autoclearMessageAttribute != nil, forceRefresh: forceReupload)
|
||||
|> mapToSignal { result -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> in
|
||||
var referenceKey: CachedSentMediaReferenceKey?
|
||||
|
|
@ -732,7 +819,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili
|
|||
}
|
||||
}
|
||||
|
||||
return .single(.progress(1.0))
|
||||
return .single(.progress(PendingMessageUploadedContentProgress(progress: 1.0)))
|
||||
|> then(
|
||||
.single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: ttlSeconds, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil)))
|
||||
)
|
||||
|
|
@ -806,13 +893,43 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili
|
|||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: nil, psaType: nil, flags: [])
|
||||
}
|
||||
var updatedAttributes = currentMessage.attributes
|
||||
if let index = updatedAttributes.firstIndex(where: { $0 is OutgoingMessageInfoAttribute }){
|
||||
let attribute = updatedAttributes[index] as! OutgoingMessageInfoAttribute
|
||||
updatedAttributes[index] = attribute.withUpdatedFlags(attribute.flags.union([.transformedMedia]))
|
||||
} else {
|
||||
updatedAttributes.append(OutgoingMessageInfoAttribute(uniqueId: Int64.random(in: Int64.min ... Int64.max), flags: [.transformedMedia], acknowledged: false, correlationId: nil, bubbleUpEmojiOrStickersets: []))
|
||||
|
||||
var markTransformedMedia = true
|
||||
var updatedMedia = currentMessage.media
|
||||
if let paidContent = updatedMedia.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent {
|
||||
var extendedMedia = paidContent.extendedMedia
|
||||
if let index = extendedMedia.firstIndex(where: { media in
|
||||
if case let .full(fullMedia) = media, fullMedia.id == id {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
extendedMedia[index] = .full(media: media)
|
||||
}
|
||||
updatedMedia = [TelegramMediaPaidContent(amount: paidContent.amount, extendedMedia: extendedMedia)]
|
||||
|
||||
if extendedMedia.contains(where: { media in
|
||||
if case .preview = media {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
markTransformedMedia = false
|
||||
}
|
||||
}
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: currentMessage.media))
|
||||
|
||||
if markTransformedMedia {
|
||||
if let index = updatedAttributes.firstIndex(where: { $0 is OutgoingMessageInfoAttribute }){
|
||||
let attribute = updatedAttributes[index] as! OutgoingMessageInfoAttribute
|
||||
updatedAttributes[index] = attribute.withUpdatedFlags(attribute.flags.union([.transformedMedia]))
|
||||
} else {
|
||||
updatedAttributes.append(OutgoingMessageInfoAttribute(uniqueId: Int64.random(in: Int64.min ... Int64.max), flags: [.transformedMedia], acknowledged: false, correlationId: nil, bubbleUpEmojiOrStickersets: []))
|
||||
}
|
||||
}
|
||||
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: updatedMedia))
|
||||
})
|
||||
}
|
||||
return .done(media)
|
||||
|
|
@ -863,7 +980,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili
|
|||
|> mapToSignal { content, fileAndThumbnailResult, resourceStatus -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> in
|
||||
guard let content = content else {
|
||||
if let resourceStatus = resourceStatus, case let .Fetching(_, progress) = resourceStatus {
|
||||
return .single(.progress(progress * 0.33))
|
||||
return .single(.progress(PendingMessageUploadedContentProgress(progress: progress * 0.33)))
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
|
|
@ -873,7 +990,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili
|
|||
if passFetchProgress {
|
||||
progress = 0.33 + progress * 0.67
|
||||
}
|
||||
return .single(.progress(progress))
|
||||
return .single(.progress(PendingMessageUploadedContentProgress(progress: progress)))
|
||||
case let .inputFile(inputFile):
|
||||
if case let .done(file, thumbnail) = fileAndThumbnailResult {
|
||||
var flags: Int32 = 0
|
||||
|
|
@ -899,7 +1016,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili
|
|||
}
|
||||
}
|
||||
|
||||
if !file.isAnimated {
|
||||
if !file.isAnimated || isPaid {
|
||||
flags |= 1 << 3
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox,
|
|||
let uploadedMedia: Signal<PendingMessageUploadedContentResult?, NoError>
|
||||
switch media {
|
||||
case .keep:
|
||||
uploadedMedia = .single(.progress(0.0))
|
||||
uploadedMedia = .single(.progress(PendingMessageUploadedContentProgress(progress: 0.0)))
|
||||
|> then(.single(nil))
|
||||
case let .update(media):
|
||||
let generateUploadSignal: (Bool) -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError>? = { forceReupload in
|
||||
|
|
@ -69,14 +69,14 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox,
|
|||
return mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, passFetchProgress: false, forceNoBigParts: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, messageId: nil, attributes: attributes, mediaReference: nil)
|
||||
}
|
||||
if let uploadSignal = generateUploadSignal(forceReupload) {
|
||||
uploadedMedia = .single(.progress(0.027))
|
||||
uploadedMedia = .single(.progress(PendingMessageUploadedContentProgress(progress: 0.027)))
|
||||
|> then(uploadSignal)
|
||||
|> map { result -> PendingMessageUploadedContentResult? in
|
||||
switch result {
|
||||
case let .progress(value):
|
||||
return .progress(max(value, 0.027))
|
||||
case let .content(content):
|
||||
return .content(content)
|
||||
case let .progress(value):
|
||||
return .progress(PendingMessageUploadedContentProgress(progress: max(value.progress, 0.027)))
|
||||
case let .content(content):
|
||||
return .content(content)
|
||||
}
|
||||
}
|
||||
|> `catch` { _ -> Signal<PendingMessageUploadedContentResult?, NoError> in
|
||||
|
|
@ -92,10 +92,10 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox,
|
|||
var pendingMediaContent: PendingMessageUploadedContent?
|
||||
if let uploadedMediaResult = uploadedMediaResult {
|
||||
switch uploadedMediaResult {
|
||||
case let .progress(value):
|
||||
return .single(.progress(value))
|
||||
case let .content(content):
|
||||
pendingMediaContent = content.content
|
||||
case let .progress(value):
|
||||
return .single(.progress(value.progress))
|
||||
case let .content(content):
|
||||
pendingMediaContent = content.content
|
||||
}
|
||||
}
|
||||
return postbox.transaction { transaction -> (Peer?, Message?, SimpleDictionary<PeerId, Peer>) in
|
||||
|
|
@ -236,7 +236,13 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox,
|
|||
} else {
|
||||
updatedFlags.remove(.Incoming)
|
||||
}
|
||||
return .update(message.withUpdatedLocalTags(updatedLocalTags).withUpdatedFlags(updatedFlags))
|
||||
|
||||
var updatedMedia = message.media
|
||||
if let previousPaidContent = previousMessage.media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent, case .full = previousPaidContent.extendedMedia.first {
|
||||
updatedMedia = previousMessage.media
|
||||
}
|
||||
|
||||
return .update(message.withUpdatedLocalTags(updatedLocalTags).withUpdatedFlags(updatedFlags).withUpdatedMedia(updatedMedia))
|
||||
})
|
||||
}
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ public func standaloneSendEnqueueMessages(
|
|||
switch result.result {
|
||||
case let .progress(value):
|
||||
allDone = false
|
||||
progressSum += value
|
||||
progressSum += value.progress
|
||||
case let .content(content):
|
||||
allResults.append((content, result.media))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3885,7 +3885,13 @@ func replayFinalState(
|
|||
if let message = locallyRenderedMessage(message: message, peers: peers) {
|
||||
generatedEvent = reactionGeneratedEvent(previousMessage.reactionsAttribute, message.reactionsAttribute, message: message, transaction: transaction)
|
||||
}
|
||||
return .update(message.withUpdatedLocalTags(updatedLocalTags).withUpdatedFlags(updatedFlags).withUpdatedAttributes(updatedAttributes))
|
||||
|
||||
var updatedMedia = message.media
|
||||
if let previousPaidContent = previousMessage.media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent, case .full = previousPaidContent.extendedMedia.first {
|
||||
updatedMedia = previousMessage.media
|
||||
}
|
||||
|
||||
return .update(message.withUpdatedLocalTags(updatedLocalTags).withUpdatedFlags(updatedFlags).withUpdatedAttributes(updatedAttributes).withUpdatedMedia(updatedMedia))
|
||||
})
|
||||
if let generatedEvent = generatedEvent {
|
||||
addedReactionEvents.append(generatedEvent)
|
||||
|
|
@ -4633,42 +4639,26 @@ func replayFinalState(
|
|||
transaction.updateMessage(messageId, update: { currentMessage in
|
||||
var media = currentMessage.media
|
||||
let invoice = media.first(where: { $0 is TelegramMediaInvoice }) as? TelegramMediaInvoice
|
||||
let currentExtendedMedia = invoice?.extendedMedia
|
||||
let paidContent = media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent
|
||||
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
|
||||
let updatedExtendedMedia: TelegramExtendedMedia?
|
||||
switch apiExtendedMedia {
|
||||
case let .messageExtendedMediaPreview(_, width, height, thumb, videoDuration):
|
||||
var dimensions: PixelDimensions?
|
||||
if let width = width, let height = height {
|
||||
dimensions = PixelDimensions(width: width, height: height)
|
||||
}
|
||||
var immediateThumbnailData: Data?
|
||||
if let thumb = thumb, case let .photoStrippedSize(_, bytes) = thumb {
|
||||
immediateThumbnailData = bytes.makeData()
|
||||
}
|
||||
updatedExtendedMedia = .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: videoDuration)
|
||||
case let .messageExtendedMedia(apiMedia):
|
||||
let (media, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId)
|
||||
if let media = media {
|
||||
updatedExtendedMedia = .full(media: media)
|
||||
} else {
|
||||
updatedExtendedMedia = currentExtendedMedia
|
||||
}
|
||||
}
|
||||
let updatedExtendedMedia = apiExtendedMedia.compactMap { TelegramExtendedMedia(apiExtendedMedia: $0, peerId: messageId.peerId) }
|
||||
|
||||
if let updatedExtendedMedia = updatedExtendedMedia, var invoice = invoice {
|
||||
if let currentExtendedMedia = currentExtendedMedia, case .full = currentExtendedMedia, case .preview = updatedExtendedMedia {
|
||||
|
||||
} else {
|
||||
if let first = updatedExtendedMedia.first, case .full = first {
|
||||
if var invoice = invoice {
|
||||
media = media.filter { !($0 is TelegramMediaInvoice) }
|
||||
invoice = invoice.withUpdatedExtendedMedia(updatedExtendedMedia)
|
||||
invoice = invoice.withUpdatedExtendedMedia(first)
|
||||
media.append(invoice)
|
||||
}
|
||||
if var paidContent = paidContent {
|
||||
media = media.filter { !($0 is TelegramMediaPaidContent) }
|
||||
paidContent = paidContent.withUpdatedExtendedMedia(updatedExtendedMedia)
|
||||
media.append(paidContent)
|
||||
}
|
||||
}
|
||||
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: media))
|
||||
|
|
|
|||
|
|
@ -47,6 +47,12 @@ func applyMediaResourceChanges(from: Media, to: Media, postbox: Postbox, force:
|
|||
if (force || fromFile.size == toFile.size || fromFile.resource.size == toFile.resource.size) && fromFile.mimeType == toFile.mimeType {
|
||||
copyOrMoveResourceData(from: fromFile.resource, to: toFile.resource, mediaBox: postbox.mediaBox)
|
||||
}
|
||||
} else if let fromPaidContent = from as? TelegramMediaPaidContent, let toPaidContent = to as? TelegramMediaPaidContent {
|
||||
for (fromMedia, toMedia) in zip(fromPaidContent.extendedMedia, toPaidContent.extendedMedia) {
|
||||
if case let .full(fullFromMedia) = fromMedia, case let .full(fullToMedia) = toMedia {
|
||||
applyMediaResourceChanges(from: fullFromMedia, to: fullToMedia, postbox: postbox, force: force)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,23 @@ import MtProtoKit
|
|||
|
||||
|
||||
public struct PendingMessageStatus: Equatable {
|
||||
public struct Progress: Equatable {
|
||||
public let progress: Float
|
||||
public let mediaProgress: [MediaId: Float]
|
||||
|
||||
public init(progress: Float, mediaProgress: [MediaId: Float] = [:]) {
|
||||
self.progress = progress
|
||||
self.mediaProgress = mediaProgress
|
||||
}
|
||||
|
||||
init(_ contentProgress: PendingMessageUploadedContentProgress) {
|
||||
self.progress = contentProgress.progress
|
||||
self.mediaProgress = contentProgress.mediaProgress
|
||||
}
|
||||
}
|
||||
|
||||
public let isRunning: Bool
|
||||
public let progress: Float
|
||||
public let progress: Progress
|
||||
}
|
||||
|
||||
private enum PendingMessageState {
|
||||
|
|
@ -364,7 +379,7 @@ public final class PendingMessageManager {
|
|||
self.messageContexts[id] = messageContext
|
||||
}
|
||||
|
||||
let status = PendingMessageStatus(isRunning: false, progress: 0.0)
|
||||
let status = PendingMessageStatus(isRunning: false, progress: PendingMessageStatus.Progress(progress: 0.0))
|
||||
if status != messageContext.status {
|
||||
messageContext.status = status
|
||||
for subscriber in messageContext.statusSubscribers.copyItems() {
|
||||
|
|
@ -618,7 +633,7 @@ public final class PendingMessageManager {
|
|||
switch next {
|
||||
case let .progress(progress):
|
||||
if let current = strongSelf.messageContexts[messageId] {
|
||||
let status = PendingMessageStatus(isRunning: true, progress: progress)
|
||||
let status = PendingMessageStatus(isRunning: true, progress: PendingMessageStatus.Progress(progress: progress))
|
||||
current.status = status
|
||||
for subscriber in current.statusSubscribers.copyItems() {
|
||||
subscriber(current.status, current.error)
|
||||
|
|
@ -636,7 +651,7 @@ public final class PendingMessageManager {
|
|||
private func beginUploadingMessage(messageContext: PendingMessageContext, id: MessageId, threadId: Int64?, groupId: Int64?, uploadSignal: Signal<PendingMessageUploadedContentResult, PendingMessageUploadError>) {
|
||||
messageContext.state = .uploading(groupId: groupId)
|
||||
|
||||
let status = PendingMessageStatus(isRunning: true, progress: 0.0)
|
||||
let status = PendingMessageStatus(isRunning: true, progress: PendingMessageStatus.Progress(progress: 0.0))
|
||||
messageContext.status = status
|
||||
for subscriber in messageContext.statusSubscribers.copyItems() {
|
||||
subscriber(messageContext.status, messageContext.error)
|
||||
|
|
@ -678,7 +693,7 @@ public final class PendingMessageManager {
|
|||
switch next {
|
||||
case let .progress(progress):
|
||||
if let current = strongSelf.messageContexts[id] {
|
||||
let status = PendingMessageStatus(isRunning: true, progress: progress)
|
||||
let status = PendingMessageStatus(isRunning: true, progress: PendingMessageStatus.Progress(progress))
|
||||
current.status = status
|
||||
for subscriber in current.statusSubscribers.copyItems() {
|
||||
subscriber(current.status, current.error)
|
||||
|
|
@ -712,7 +727,7 @@ public final class PendingMessageManager {
|
|||
if case let .waitingForUploadToStart(groupId, uploadSignal) = context.state {
|
||||
if self.canBeginUploadingMessage(id: contextId, type: context.contentType ?? .media) {
|
||||
context.state = .uploading(groupId: groupId)
|
||||
let status = PendingMessageStatus(isRunning: true, progress: 0.0)
|
||||
let status = PendingMessageStatus(isRunning: true, progress: PendingMessageStatus.Progress(progress: 0.0))
|
||||
context.status = status
|
||||
for subscriber in context.statusSubscribers.copyItems() {
|
||||
subscriber(context.status, context.error)
|
||||
|
|
@ -734,7 +749,7 @@ public final class PendingMessageManager {
|
|||
switch next {
|
||||
case let .progress(progress):
|
||||
if let current = strongSelf.messageContexts[contextId] {
|
||||
let status = PendingMessageStatus(isRunning: true, progress: progress)
|
||||
let status = PendingMessageStatus(isRunning: true, progress: PendingMessageStatus.Progress(progress))
|
||||
current.status = status
|
||||
for subscriber in current.statusSubscribers.copyItems() {
|
||||
subscriber(context.status, context.error)
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
|
|||
|
||||
public class Serialization: NSObject, MTSerialization {
|
||||
public func currentLayer() -> UInt {
|
||||
return 182
|
||||
return 183
|
||||
}
|
||||
|
||||
public func parseMessage(_ data: Data!) -> Any! {
|
||||
|
|
|
|||
|
|
@ -322,3 +322,26 @@ func _internal_requestStarsRevenueWithdrawalUrl(account: Account, peerId: PeerId
|
|||
|> mapError { _ -> RequestStarsRevenueWithdrawalError in }
|
||||
|> switchToLatest
|
||||
}
|
||||
|
||||
func _internal_requestStarsRevenueAdsAccountlUrl(account: Account, peerId: EnginePeer.Id) -> Signal<String?, NoError> {
|
||||
return account.postbox.transaction { transaction -> Signal<String?, NoError> in
|
||||
guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else {
|
||||
return .single(nil)
|
||||
}
|
||||
return account.network.request(Api.functions.payments.getStarsRevenueAdsAccountUrl(peer: inputPeer))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { error -> Signal<Api.payments.StarsRevenueAdsAccountUrl?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> map { result -> String? in
|
||||
guard let result else {
|
||||
return nil
|
||||
}
|
||||
switch result {
|
||||
case let .starsRevenueAdsAccountUrl(url):
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
|> switchToLatest
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import Foundation
|
||||
import Postbox
|
||||
|
||||
public final class TelegramMediaPaidContent: Media, Equatable {
|
||||
public var peerIds: [PeerId] = []
|
||||
|
||||
public var id: MediaId? {
|
||||
return nil
|
||||
}
|
||||
|
||||
public let amount: Int64
|
||||
public let extendedMedia: [TelegramExtendedMedia]
|
||||
|
||||
public init(amount: Int64, extendedMedia: [TelegramExtendedMedia]) {
|
||||
self.amount = amount
|
||||
self.extendedMedia = extendedMedia
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.amount = decoder.decodeInt64ForKey("a", orElse: 0)
|
||||
self.extendedMedia = (try? decoder.decodeObjectArrayWithCustomDecoderForKey("m", decoder: { TelegramExtendedMedia(decoder: $0) })) ?? []
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt64(self.amount, forKey: "a")
|
||||
encoder.encodeObjectArray(self.extendedMedia, forKey: "m")
|
||||
}
|
||||
|
||||
public static func ==(lhs: TelegramMediaPaidContent, rhs: TelegramMediaPaidContent) -> Bool {
|
||||
return lhs.isEqual(to: rhs)
|
||||
}
|
||||
|
||||
public func isEqual(to other: Media) -> Bool {
|
||||
guard let other = other as? TelegramMediaPaidContent else {
|
||||
return false
|
||||
}
|
||||
|
||||
if self.amount != other.amount {
|
||||
return false
|
||||
}
|
||||
|
||||
if self.extendedMedia != other.extendedMedia {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
public func isSemanticallyEqual(to other: Media) -> Bool {
|
||||
return self.isEqual(to: other)
|
||||
}
|
||||
|
||||
public func withUpdatedExtendedMedia(_ extendedMedia: [TelegramExtendedMedia]) -> TelegramMediaPaidContent {
|
||||
return TelegramMediaPaidContent(
|
||||
amount: self.amount,
|
||||
extendedMedia: extendedMedia
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import Foundation
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramApi
|
||||
import MtProtoKit
|
||||
|
||||
private func _internal_updateExtendedMediaById(account: Account, peerId: EnginePeer.Id, messageIds: [EngineMessage.Id]) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> Peer? in
|
||||
if let peer = transaction.getPeer(peerId) {
|
||||
return peer
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|> mapToSignal { peer -> Signal<Never, NoError> in
|
||||
guard let peer = peer, let inputPeer = apiInputPeer(peer) else {
|
||||
return .complete()
|
||||
}
|
||||
return account.network.request(Api.functions.messages.getExtendedMedia(peer: inputPeer, id: messageIds.map { $0.id }))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { updates -> Signal<Never, NoError> in
|
||||
if let updates = updates {
|
||||
account.stateManager.addUpdates(updates)
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_updateExtendedMedia(account: Account, messageIds: [EngineMessage.Id]) -> Signal<Never, NoError> {
|
||||
var signals: [Signal<Never, NoError>] = []
|
||||
for (peerId, messageIds) in messagesIdsGroupedByPeerId(messageIds) {
|
||||
signals.append(_internal_updateExtendedMediaById(account: account, peerId: peerId, messageIds: messageIds))
|
||||
}
|
||||
return combineLatest(signals)
|
||||
|> ignoreValues
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ public enum EngineMedia: Equatable {
|
|||
case story(TelegramMediaStory)
|
||||
case giveaway(TelegramMediaGiveaway)
|
||||
case giveawayResults(TelegramMediaGiveawayResults)
|
||||
case paidContent(TelegramMediaPaidContent)
|
||||
}
|
||||
|
||||
public extension EngineMedia {
|
||||
|
|
@ -56,6 +57,8 @@ public extension EngineMedia {
|
|||
return giveaway.id
|
||||
case let .giveawayResults(giveawayResults):
|
||||
return giveawayResults.id
|
||||
case let .paidContent(paidContent):
|
||||
return paidContent.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -95,6 +98,8 @@ public extension EngineMedia {
|
|||
self = .giveaway(giveaway)
|
||||
case let giveawayResults as TelegramMediaGiveawayResults:
|
||||
self = .giveawayResults(giveawayResults)
|
||||
case let paidContent as TelegramMediaPaidContent:
|
||||
self = .paidContent(paidContent)
|
||||
default:
|
||||
preconditionFailure()
|
||||
}
|
||||
|
|
@ -134,6 +139,8 @@ public extension EngineMedia {
|
|||
return giveaway
|
||||
case let .giveawayResults(giveawayResults):
|
||||
return giveawayResults
|
||||
case let .paidContent(paidContent):
|
||||
return paidContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1093,7 +1093,7 @@ func _internal_uploadStoryImpl(
|
|||
|> mapToSignal { result -> Signal<StoryUploadResult, NoError> in
|
||||
switch result {
|
||||
case let .progress(progress):
|
||||
return .single(.progress(progress))
|
||||
return .single(.progress(progress.progress))
|
||||
case let .content(content):
|
||||
return postbox.transaction { transaction -> Signal<StoryUploadResult, NoError> in
|
||||
let privacyRules = apiInputPrivacyRules(privacy: privacy, transaction: transaction)
|
||||
|
|
@ -1278,7 +1278,7 @@ func _internal_editStory(account: Account, peerId: PeerId, id: Int32, media: Eng
|
|||
return contentSignal
|
||||
|> mapToSignal { result -> Signal<StoryUploadResult, NoError> in
|
||||
if let result = result, case let .progress(progress) = result {
|
||||
return .single(.progress(progress))
|
||||
return .single(.progress(progress.progress))
|
||||
}
|
||||
|
||||
let inputMedia: Api.InputMedia?
|
||||
|
|
|
|||
|
|
@ -1431,6 +1431,10 @@ public extension TelegramEngine {
|
|||
return _internal_reportAdMessage(account: self.account, peerId: peerId, opaqueId: opaqueId, option: option)
|
||||
}
|
||||
|
||||
public func updateExtendedMedia(messageIds: [EngineMessage.Id]) -> Signal<Never, NoError> {
|
||||
return _internal_updateExtendedMedia(account: self.account, messageIds: messageIds)
|
||||
}
|
||||
|
||||
public func getAllLocalChannels(count: Int) -> Signal<[EnginePeer.Id], NoError> {
|
||||
return self.account.postbox.transaction { transaction -> [EnginePeer.Id] in
|
||||
var result: [EnginePeer.Id] = []
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -199,7 +199,7 @@ private final class StarsContextImpl {
|
|||
return
|
||||
}
|
||||
var transactions = state.transactions
|
||||
transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil), at: 0)
|
||||
transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, media: []), at: 0)
|
||||
|
||||
self.updateState(StarsContext.State(flags: [.isPendingBalance], balance: state.balance + balance, transactions: transactions, canLoadMore: state.canLoadMore, isLoading: state.isLoading))
|
||||
}
|
||||
|
|
@ -236,10 +236,11 @@ 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):
|
||||
case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia):
|
||||
let parsedPeer: StarsContext.State.Transaction.Peer
|
||||
var paidMessageId: MessageId?
|
||||
switch transactionPeer {
|
||||
case .starsTransactionPeerAppStore:
|
||||
parsedPeer = .appStore
|
||||
|
|
@ -249,6 +250,8 @@ private extension StarsContext.State.Transaction {
|
|||
parsedPeer = .fragment
|
||||
case .starsTransactionPeerPremiumBot:
|
||||
parsedPeer = .premiumBot
|
||||
case .starsTransactionPeerAds:
|
||||
parsedPeer = .ads
|
||||
case .starsTransactionPeerUnsupported:
|
||||
parsedPeer = .unsupported
|
||||
case let .starsTransactionPeer(apiPeer):
|
||||
|
|
@ -256,13 +259,28 @@ private extension StarsContext.State.Transaction {
|
|||
return nil
|
||||
}
|
||||
parsedPeer = .peer(EnginePeer(peer))
|
||||
if let 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var flags: Flags = []
|
||||
if (apiFlags & (1 << 3)) != 0 {
|
||||
flags.insert(.isRefund)
|
||||
}
|
||||
self.init(flags: flags, id: id, count: stars, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl)
|
||||
if (apiFlags & (1 << 4)) != 0 {
|
||||
flags.insert(.isPending)
|
||||
}
|
||||
if (apiFlags & (1 << 6)) != 0 {
|
||||
flags.insert(.isFailed)
|
||||
}
|
||||
|
||||
let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? []
|
||||
self.init(flags: flags, id: id, count: stars, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, media: media)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -280,6 +298,7 @@ public final class StarsContext {
|
|||
public static let isRefund = Flags(rawValue: 1 << 0)
|
||||
public static let isLocal = Flags(rawValue: 1 << 1)
|
||||
public static let isPending = Flags(rawValue: 1 << 2)
|
||||
public static let isFailed = Flags(rawValue: 1 << 3)
|
||||
}
|
||||
|
||||
public enum Peer: Equatable {
|
||||
|
|
@ -287,6 +306,7 @@ public final class StarsContext {
|
|||
case playMarket
|
||||
case fragment
|
||||
case premiumBot
|
||||
case ads
|
||||
case unsupported
|
||||
case peer(EnginePeer)
|
||||
}
|
||||
|
|
@ -301,6 +321,8 @@ public final class StarsContext {
|
|||
public let photo: TelegramMediaWebFile?
|
||||
public let transactionDate: Int32?
|
||||
public let transactionUrl: String?
|
||||
public let paidMessageId: MessageId?
|
||||
public let media: [Media]
|
||||
|
||||
public init(
|
||||
flags: Flags,
|
||||
|
|
@ -312,7 +334,9 @@ public final class StarsContext {
|
|||
description: String?,
|
||||
photo: TelegramMediaWebFile?,
|
||||
transactionDate: Int32?,
|
||||
transactionUrl: String?
|
||||
transactionUrl: String?,
|
||||
paidMessageId: MessageId?,
|
||||
media: [Media]
|
||||
) {
|
||||
self.flags = flags
|
||||
self.id = id
|
||||
|
|
@ -324,6 +348,48 @@ public final class StarsContext {
|
|||
self.photo = photo
|
||||
self.transactionDate = transactionDate
|
||||
self.transactionUrl = transactionUrl
|
||||
self.paidMessageId = paidMessageId
|
||||
self.media = media
|
||||
}
|
||||
|
||||
public static func == (lhs: Transaction, rhs: Transaction) -> Bool {
|
||||
if lhs.flags != rhs.flags {
|
||||
return false
|
||||
}
|
||||
if lhs.id != rhs.id {
|
||||
return false
|
||||
}
|
||||
if lhs.count != rhs.count {
|
||||
return false
|
||||
}
|
||||
if lhs.date != rhs.date {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.description != rhs.description {
|
||||
return false
|
||||
}
|
||||
if lhs.photo != rhs.photo {
|
||||
return false
|
||||
}
|
||||
if lhs.transactionDate != rhs.transactionDate {
|
||||
return false
|
||||
}
|
||||
if lhs.transactionUrl != rhs.transactionUrl {
|
||||
return false
|
||||
}
|
||||
if lhs.paidMessageId != rhs.paidMessageId {
|
||||
return false
|
||||
}
|
||||
if !areMediaArraysEqual(lhs.media, rhs.media) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -535,8 +601,8 @@ private final class StarsTransactionsContextImpl {
|
|||
var updatedState = self._state
|
||||
updatedState.isLoading = true
|
||||
self.updateState(updatedState)
|
||||
|
||||
self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, mode: self.mode, offset: nextOffset, limit: 10)
|
||||
|
||||
self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, mode: self.mode, offset: nextOffset, limit: self.nextOffset == "" ? 25 : 50)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
guard let self else {
|
||||
return
|
||||
|
|
@ -690,6 +756,8 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot
|
|||
return .fail(.paymentFailed)
|
||||
} else if error.errorDescription == "INVOICE_ALREADY_PAID" {
|
||||
return .fail(.alreadyPaid)
|
||||
} else if error.errorDescription == "MEDIA_ALREADY_PAID" {
|
||||
return .fail(.alreadyPaid)
|
||||
}
|
||||
return .fail(.generic)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,10 @@ public extension TelegramEngine {
|
|||
return StarsContext(account: self.account)
|
||||
}
|
||||
|
||||
public func peerStarsRevenueContext(peerId: EnginePeer.Id) -> StarsRevenueStatsContext {
|
||||
return StarsRevenueStatsContext(account: self.account, peerId: peerId)
|
||||
}
|
||||
|
||||
public func peerStarsTransactionsContext(subject: StarsTransactionsContext.Subject, mode: StarsTransactionsContext.Mode) -> StarsTransactionsContext {
|
||||
return StarsTransactionsContext(account: self.account, subject: subject, mode: mode)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -831,6 +831,10 @@ public extension TelegramEngine {
|
|||
return _internal_requestStarsRevenueWithdrawalUrl(account: self.account, peerId: peerId, amount: amount, password: password)
|
||||
}
|
||||
|
||||
public func requestStarsRevenueAdsAccountlUrl(peerId: EnginePeer.Id) -> Signal<String?, NoError> {
|
||||
return _internal_requestStarsRevenueAdsAccountlUrl(account: self.account, peerId: peerId)
|
||||
}
|
||||
|
||||
public func getChatListPeers(filterPredicate: ChatListFilterPredicate) -> Signal<[EnginePeer], NoError> {
|
||||
return self.account.postbox.transaction { transaction -> [EnginePeer] in
|
||||
return transaction.getChatListPeers(groupId: .root, filterPredicate: filterPredicate, additionalFilter: nil).map(EnginePeer.init)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ public enum MessageContentKindKey {
|
|||
case invoice
|
||||
case story
|
||||
case giveaway
|
||||
case paidContent
|
||||
}
|
||||
|
||||
public enum MessageContentKind: Equatable {
|
||||
|
|
@ -386,6 +387,25 @@ public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil
|
|||
} else {
|
||||
return nil
|
||||
}
|
||||
case let .paidContent(paidContent):
|
||||
switch paidContent.extendedMedia.first {
|
||||
case let .preview(_, _, videoDuration):
|
||||
if let _ = videoDuration {
|
||||
return .video
|
||||
} else {
|
||||
return .image
|
||||
}
|
||||
case let .full(media):
|
||||
if media is TelegramMediaImage {
|
||||
return .image
|
||||
} else if media is TelegramMediaFile {
|
||||
return .video
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -477,25 +477,22 @@ public final class ChatBotInfoItemNode: ListViewItemNode {
|
|||
case .longTap, .doubleTap:
|
||||
if let item = self.item, self.backgroundNode.frame.contains(location) {
|
||||
let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false)
|
||||
//TODO:do
|
||||
let _ = item
|
||||
let _ = tapAction
|
||||
// switch tapAction.content {
|
||||
// case .none, .ignore:
|
||||
// break
|
||||
// case let .url(url):
|
||||
// item.controllerInteraction.longTap(.url(url.url), nil)
|
||||
// case let .peerMention(peerId, mention, _):
|
||||
// item.controllerInteraction.longTap(.peerMention(peerId, mention), nil)
|
||||
// case let .textMention(name):
|
||||
// item.controllerInteraction.longTap(.mention(name), nil)
|
||||
// case let .botCommand(command):
|
||||
// item.controllerInteraction.longTap(.command(command), nil)
|
||||
// case let .hashtag(_, hashtag):
|
||||
// item.controllerInteraction.longTap(.hashtag(hashtag), nil)
|
||||
// default:
|
||||
// break
|
||||
// }
|
||||
switch tapAction.content {
|
||||
case .none, .ignore:
|
||||
break
|
||||
case let .url(url):
|
||||
item.controllerInteraction.longTap(.url(url.url), ChatControllerInteraction.LongTapParams())
|
||||
case let .peerMention(peerId, mention, _):
|
||||
item.controllerInteraction.longTap(.peerMention(peerId, mention), ChatControllerInteraction.LongTapParams())
|
||||
case let .textMention(name):
|
||||
item.controllerInteraction.longTap(.mention(name), ChatControllerInteraction.LongTapParams())
|
||||
case let .botCommand(command):
|
||||
item.controllerInteraction.longTap(.command(command), ChatControllerInteraction.LongTapParams())
|
||||
case let .hashtag(_, hashtag):
|
||||
item.controllerInteraction.longTap(.hashtag(hashtag), ChatControllerInteraction.LongTapParams())
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
|
|
|||
|
|
@ -408,6 +408,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
|
|||
attributes,
|
||||
contentMediaValue,
|
||||
nil,
|
||||
nil,
|
||||
automaticDownload ? .full : .none,
|
||||
associatedData.automaticDownloadPeerType,
|
||||
associatedData.automaticDownloadPeerId,
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ public enum ChatMessageBubbleContentPosition {
|
|||
|
||||
public enum ChatMessageBubblePreparePosition {
|
||||
case linear(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition)
|
||||
case mosaic(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition)
|
||||
case mosaic(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition, index: Int?)
|
||||
}
|
||||
|
||||
public struct ChatMessageBubbleContentTapAction {
|
||||
|
|
@ -208,6 +208,8 @@ open class ChatMessageBubbleContentNode: ASDisplayNode {
|
|||
return false
|
||||
}
|
||||
|
||||
open var index: Int?
|
||||
|
||||
public weak var itemNode: ChatMessageItemNodeProtocol?
|
||||
public weak var bubbleBackgroundNode: ChatMessageBackground?
|
||||
public weak var bubbleBackdropNode: ChatMessageBubbleBackdrop?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -78,9 +80,17 @@ import TelegramAnimatedStickerNode
|
|||
import LottieMetal
|
||||
|
||||
private struct BubbleItemAttributes {
|
||||
var index: Int?
|
||||
var isAttachment: Bool
|
||||
var neighborType: ChatMessageBubbleRelativePosition.NeighbourType
|
||||
var neighborSpacing: ChatMessageBubbleRelativePosition.NeighbourSpacing
|
||||
|
||||
init(index: Int? = nil, isAttachment: Bool, neighborType: ChatMessageBubbleRelativePosition.NeighbourType, neighborSpacing: ChatMessageBubbleRelativePosition.NeighbourSpacing) {
|
||||
self.index = index
|
||||
self.isAttachment = isAttachment
|
||||
self.neighborType = neighborType
|
||||
self.neighborSpacing = neighborSpacing
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChatMessageBubbleClippingNode: ASDisplayNode {
|
||||
|
|
@ -123,7 +133,13 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
|||
|
||||
var isFile = false
|
||||
inner: for media in message.media {
|
||||
if let _ = media as? TelegramMediaImage {
|
||||
if let media = media as? TelegramMediaPaidContent {
|
||||
var index = 0
|
||||
for _ in media.extendedMedia {
|
||||
result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(index: index, isAttachment: false, neighborType: .media, neighborSpacing: .default)))
|
||||
index += 1
|
||||
}
|
||||
} else if let _ = media as? TelegramMediaImage {
|
||||
if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), message.text.isEmpty {
|
||||
messageWithCaptionToAdd = (message, itemAttributes)
|
||||
}
|
||||
|
|
@ -622,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
|
||||
|
|
@ -666,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()
|
||||
|
|
@ -1184,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
|
||||
|
|
@ -1334,10 +1362,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
}
|
||||
|
||||
override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) {
|
||||
var currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))] = []
|
||||
var currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, Int?, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))] = []
|
||||
for contentNode in self.contentNodes {
|
||||
if let message = contentNode.item?.message {
|
||||
currentContentClassesPropertiesAndLayouts.append((message, type(of: contentNode) as AnyClass, contentNode.supportsMosaic, contentNode.asyncLayoutContent()))
|
||||
currentContentClassesPropertiesAndLayouts.append((message, type(of: contentNode) as AnyClass, contentNode.supportsMosaic, contentNode.index, contentNode.asyncLayoutContent()))
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
|
|
@ -1352,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)
|
||||
|
||||
|
|
@ -1377,6 +1407,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
replyInfoLayout: replyInfoLayout,
|
||||
actionButtonsLayout: actionButtonsLayout,
|
||||
reactionButtonsLayout: reactionButtonsLayout,
|
||||
unlockButtonLayout: unlockButtonLayout,
|
||||
mediaInfoLayout: mediaInfoLayout,
|
||||
mosaicStatusLayout: mosaicStatusLayout,
|
||||
layoutConstants: layoutConstants,
|
||||
currentItem: currentItem,
|
||||
|
|
@ -1388,7 +1420,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
|
||||
private static func beginLayout(
|
||||
selfReference: Weak<ChatMessageBubbleItemNode>, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool,
|
||||
currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))],
|
||||
currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, Int?, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))],
|
||||
authorNameLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode),
|
||||
viaMeasureLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode),
|
||||
adminBadgeLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode),
|
||||
|
|
@ -1398,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?,
|
||||
|
|
@ -1699,15 +1733,15 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
maximumContentWidth = max(0.0, maximumContentWidth)
|
||||
|
||||
var contentPropertiesAndPrepareLayouts: [(Message, Bool, ChatMessageEntryAttributes, BubbleItemAttributes, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))] = []
|
||||
var addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode)]?
|
||||
var addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode, Int?)]?
|
||||
for contentNodeItemValue in contentNodeMessagesAndClasses {
|
||||
let contentNodeItem = contentNodeItemValue as (message: Message, type: AnyClass, attributes: ChatMessageEntryAttributes, bubbleAttributes: BubbleItemAttributes)
|
||||
|
||||
var found = false
|
||||
for currentNodeItemValue in currentContentClassesPropertiesAndLayouts {
|
||||
let currentNodeItem = currentNodeItemValue as (message: Message, type: AnyClass, supportsMosaic: Bool, currentLayout: (ChatMessageBubbleContentItem, ChatMessageItemLayoutConstants, ChatMessageBubblePreparePosition, Bool?, CGSize, CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))
|
||||
let currentNodeItem = currentNodeItemValue as (message: Message, type: AnyClass, supportsMosaic: Bool, index: Int?, currentLayout: (ChatMessageBubbleContentItem, ChatMessageItemLayoutConstants, ChatMessageBubblePreparePosition, Bool?, CGSize, CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))
|
||||
|
||||
if currentNodeItem.type == contentNodeItem.type && currentNodeItem.message.stableId == contentNodeItem.message.stableId {
|
||||
if currentNodeItem.type == contentNodeItem.type && currentNodeItem.index == contentNodeItem.bubbleAttributes.index && currentNodeItem.message.stableId == contentNodeItem.message.stableId {
|
||||
contentPropertiesAndPrepareLayouts.append((contentNodeItem.message, currentNodeItem.supportsMosaic, contentNodeItem.attributes, contentNodeItem.bubbleAttributes, currentNodeItem.currentLayout))
|
||||
found = true
|
||||
break
|
||||
|
|
@ -1715,11 +1749,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
}
|
||||
if !found {
|
||||
let contentNode = (contentNodeItem.type as! ChatMessageBubbleContentNode.Type).init()
|
||||
contentNode.index = contentNodeItem.bubbleAttributes.index
|
||||
contentPropertiesAndPrepareLayouts.append((contentNodeItem.message, contentNode.supportsMosaic, contentNodeItem.attributes, contentNodeItem.bubbleAttributes, contentNode.asyncLayoutContent()))
|
||||
if addedContentNodes == nil {
|
||||
addedContentNodes = []
|
||||
}
|
||||
addedContentNodes!.append((contentNodeItem.message, contentNodeItem.bubbleAttributes.isAttachment, contentNode))
|
||||
addedContentNodes!.append((contentNodeItem.message, contentNodeItem.bubbleAttributes.isAttachment, contentNode, contentNodeItem.bubbleAttributes.index))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1924,7 +1959,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
|
||||
let prepareContentPosition: ChatMessageBubblePreparePosition
|
||||
if let mosaicRange = mosaicRange, mosaicRange.contains(index) {
|
||||
prepareContentPosition = .mosaic(top: .None(.None(.Incoming)), bottom: index == (mosaicRange.upperBound - 1) ? bottomPosition : .None(.None(.Incoming)))
|
||||
let mosaicIndex = index - mosaicRange.lowerBound
|
||||
prepareContentPosition = .mosaic(top: .None(.None(.Incoming)), bottom: index == (mosaicRange.upperBound - 1) ? bottomPosition : .None(.None(.Incoming)), index: mosaicIndex)
|
||||
} else {
|
||||
let refinedBottomPosition: ChatMessageBubbleRelativePosition
|
||||
if index == contentPropertiesAndPrepareLayouts.count - 1 {
|
||||
|
|
@ -2169,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
|
||||
|
|
@ -2270,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 {
|
||||
|
|
@ -2808,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
|
||||
|
|
@ -2823,7 +2864,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
if let mosaicRange = mosaicRange, mosaicRange.contains(i), let (framesAndPositions, size) = calculatedGroupFramesAndSize {
|
||||
let mosaicIndex = i - mosaicRange.lowerBound
|
||||
|
||||
if mosaicIndex == 0 {
|
||||
if mosaicIndex == 0 && i == 0 {
|
||||
if !headerSize.height.isZero {
|
||||
contentNodesHeight += 7.0
|
||||
totalContentNodesHeight += 7.0
|
||||
|
|
@ -2835,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
|
||||
|
|
@ -2854,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
|
||||
}
|
||||
|
|
@ -2869,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
|
||||
|
|
@ -2893,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)
|
||||
|
|
@ -3023,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,
|
||||
|
|
@ -3071,12 +3157,16 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
replyInfoOriginY: CGFloat,
|
||||
removedContentNodeIndices: [Int]?,
|
||||
updatedContentNodeOrder: Bool,
|
||||
addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode)]?,
|
||||
addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode, Int?)]?,
|
||||
contentNodeMessagesAndClasses: [(Message, AnyClass, ChatMessageEntryAttributes, BubbleItemAttributes)],
|
||||
contentNodeFramesPropertiesAndApply: [(CGRect, ChatMessageBubbleContentProperties, Bool, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void)],
|
||||
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?,
|
||||
|
|
@ -3876,7 +3966,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
}
|
||||
|
||||
if let addedContentNodes = addedContentNodes {
|
||||
for (contentNodeMessage, isAttachment, contentNode) in addedContentNodes {
|
||||
for (contentNodeMessage, isAttachment, contentNode, _ ) in addedContentNodes {
|
||||
updatedContentNodes.append(contentNode)
|
||||
|
||||
let contextSourceNode: ContextExtractedContentContainingNode
|
||||
|
|
@ -3908,18 +3998,17 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
|
||||
var sortedContentNodes: [ChatMessageBubbleContentNode] = []
|
||||
outer: for contentItemValue in contentNodeMessagesAndClasses {
|
||||
let contentItem = contentItemValue as (message: Message, type: AnyClass, ChatMessageEntryAttributes, BubbleItemAttributes)
|
||||
|
||||
let contentItem = contentItemValue as (message: Message, type: AnyClass, ChatMessageEntryAttributes, attributes: BubbleItemAttributes)
|
||||
if let addedContentNodes = addedContentNodes {
|
||||
for (contentNodeMessage, _, contentNode) in addedContentNodes {
|
||||
if type(of: contentNode) == contentItem.type && contentNodeMessage.stableId == contentItem.message.stableId {
|
||||
for (contentNodeMessage, _, contentNode, index) in addedContentNodes {
|
||||
if type(of: contentNode) == contentItem.type && index == contentItem.attributes.index && contentNodeMessage.stableId == contentItem.message.stableId {
|
||||
sortedContentNodes.append(contentNode)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
for contentNode in updatedContentNodes {
|
||||
if type(of: contentNode) == contentItem.type && contentNode.item?.message.stableId == contentItem.message.stableId {
|
||||
if type(of: contentNode) == contentItem.type && contentNode.index == contentItem.attributes.index && contentNode.item?.message.stableId == contentItem.message.stableId {
|
||||
sortedContentNodes.append(contentNode)
|
||||
continue outer
|
||||
}
|
||||
|
|
@ -4081,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 {
|
||||
|
|
@ -5142,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 {
|
||||
|
|
@ -5154,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5170,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
|
||||
}
|
||||
|
|
@ -5757,6 +5911,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
item.controllerInteraction.openMessageContextMenu(item.message, true, self, subFrame, nil, nil)
|
||||
}
|
||||
|
||||
override public func makeProgress() -> Promise<Bool>? {
|
||||
return self.unlockButtonNode?.makeProgress()
|
||||
}
|
||||
|
||||
override public func targetReactionView(value: MessageReaction.Reaction) -> UIView? {
|
||||
if let result = self.reactionButtonsNode?.reactionTargetView(value: value) {
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -1490,7 +1490,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco
|
|||
reactionButtonsNode.offset(value: value, animationCurve: animationCurve, duration: duration)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override public func targetReactionView(value: MessageReaction.Reaction) -> UIView? {
|
||||
if let result = self.reactionButtonsNode?.reactionTargetView(value: value) {
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -473,7 +473,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||
updatedPlaybackStatus = combineLatest(messageFileMediaResourceStatus(context: item.context, file: updatedFile, message: EngineMessage(item.message), isRecentActions: item.associatedData.isRecentActions), item.context.account.pendingMessageManager.pendingMessageStatus(item.message.id) |> map { $0.0 })
|
||||
|> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in
|
||||
if let pendingStatus = pendingStatus {
|
||||
var progress = pendingStatus.progress
|
||||
var progress = pendingStatus.progress.progress
|
||||
if pendingStatus.isRunning {
|
||||
progress = max(progress, 0.27)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -445,6 +445,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
private var message: Message?
|
||||
private var attributes: ChatMessageEntryAttributes?
|
||||
private var media: Media?
|
||||
private var mediaIndex: Int?
|
||||
private var themeAndStrings: (PresentationTheme, PresentationStrings, String, Bool)?
|
||||
private var sizeCalculation: InteractiveMediaNodeSizeCalculation?
|
||||
private var wideLayout: Bool?
|
||||
|
|
@ -735,6 +736,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
} else {
|
||||
if let invoice = self.media as? TelegramMediaInvoice, let _ = invoice.extendedMedia {
|
||||
self.activateLocalContent(.default)
|
||||
} else if let _ = self.media as? TelegramMediaPaidContent {
|
||||
self.activateLocalContent(.default)
|
||||
} else if let storyMedia = media as? TelegramMediaStory, let storyItem = self.message?.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) {
|
||||
if case let .item(item) = storyItem, let _ = item.media {
|
||||
self.activateLocalContent(.default)
|
||||
|
|
@ -747,7 +750,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
}
|
||||
}
|
||||
|
||||
public func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ peerId: EnginePeer.Id?, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
|
||||
public func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ mediaIndex: Int?, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ peerId: EnginePeer.Id?, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
|
||||
let currentMessage = self.message
|
||||
let currentMedia = self.media
|
||||
let imageLayout = self.imageNode.asyncLayout()
|
||||
|
|
@ -761,7 +764,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
let currentAutomaticDownload = self.automaticDownload
|
||||
let currentAutomaticPlayback = self.automaticPlayback
|
||||
|
||||
return { [weak self] context, presentationData, dateTimeFormat, message, associatedData, attributes, media, dateAndStatus, automaticDownload, peerType, peerId, sizeCalculation, layoutConstants, contentMode, presentationContext in
|
||||
return { [weak self] context, presentationData, dateTimeFormat, message, associatedData, attributes, media, mediaIndex, dateAndStatus, automaticDownload, peerType, peerId, sizeCalculation, layoutConstants, contentMode, presentationContext in
|
||||
let _ = peerType
|
||||
|
||||
var nativeSize: CGSize
|
||||
|
|
@ -868,8 +871,18 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
case .color, .gradient, .emoticon:
|
||||
unboundSize = CGSize(width: 128.0, height: 128.0)
|
||||
}
|
||||
} else if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia {
|
||||
switch extendedMedia {
|
||||
} else {
|
||||
var extendedMedia: TelegramExtendedMedia?
|
||||
if let invoice = media as? TelegramMediaInvoice, let selectedMedia = invoice.extendedMedia {
|
||||
extendedMedia = selectedMedia
|
||||
} else if let paidContent = media as? TelegramMediaPaidContent {
|
||||
let selectedMediaIndex = mediaIndex ?? 0
|
||||
if selectedMediaIndex < paidContent.extendedMedia.count {
|
||||
extendedMedia = paidContent.extendedMedia[selectedMediaIndex]
|
||||
}
|
||||
}
|
||||
if let extendedMedia {
|
||||
switch extendedMedia {
|
||||
case let .preview(dimensions, _, _):
|
||||
if let dimensions = dimensions {
|
||||
unboundSize = CGSize(width: max(10.0, floor(dimensions.cgSize.width * 0.5)), height: max(10.0, floor(dimensions.cgSize.height * 0.5)))
|
||||
|
|
@ -903,9 +916,10 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
} else {
|
||||
unboundSize = CGSize(width: 54.0, height: 54.0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unboundSize = CGSize(width: 54.0, height: 54.0)
|
||||
}
|
||||
} else {
|
||||
unboundSize = CGSize(width: 54.0, height: 54.0)
|
||||
}
|
||||
|
||||
switch sizeCalculation {
|
||||
|
|
@ -1113,7 +1127,18 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
|
||||
if mediaUpdated || isSendingUpdated || automaticPlaybackUpdated {
|
||||
var media = media
|
||||
if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia {
|
||||
|
||||
var extendedMedia: TelegramExtendedMedia?
|
||||
if let invoice = media as? TelegramMediaInvoice, let selectedMedia = invoice.extendedMedia {
|
||||
extendedMedia = selectedMedia
|
||||
} else if let paidContent = media as? TelegramMediaPaidContent {
|
||||
let selectedMediaIndex = mediaIndex ?? 0
|
||||
if selectedMediaIndex < paidContent.extendedMedia.count {
|
||||
extendedMedia = paidContent.extendedMedia[selectedMediaIndex]
|
||||
}
|
||||
}
|
||||
|
||||
if let extendedMedia {
|
||||
switch extendedMedia {
|
||||
case let .preview(_, immediateThumbnailData, _):
|
||||
let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
||||
|
|
@ -1441,7 +1466,16 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
var isExtendedMedia = false
|
||||
if statusUpdated {
|
||||
var media = media
|
||||
if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia {
|
||||
var extendedMedia: TelegramExtendedMedia?
|
||||
if let invoice = media as? TelegramMediaInvoice, let selectedMedia = invoice.extendedMedia {
|
||||
extendedMedia = selectedMedia
|
||||
} else if let paidContent = media as? TelegramMediaPaidContent {
|
||||
let selectedMediaIndex = mediaIndex ?? 0
|
||||
if selectedMediaIndex < paidContent.extendedMedia.count {
|
||||
extendedMedia = paidContent.extendedMedia[selectedMediaIndex]
|
||||
}
|
||||
}
|
||||
if let extendedMedia, case let .full(fullMedia) = extendedMedia {
|
||||
isExtendedMedia = true
|
||||
media = fullMedia
|
||||
}
|
||||
|
|
@ -1456,7 +1490,11 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
updatedStatusSignal = combineLatest(chatMessagePhotoStatus(context: context, messageId: message.id, photoReference: .message(message: MessageReference(message), media: image)), context.account.pendingMessageManager.pendingMessageStatus(message.id) |> map { $0.0 })
|
||||
|> map { resourceStatus, pendingStatus -> (MediaResourceStatus, MediaResourceStatus?) in
|
||||
if let pendingStatus = pendingStatus {
|
||||
let adjustedProgress = max(pendingStatus.progress, 0.027)
|
||||
var progress: Float = pendingStatus.progress.progress
|
||||
if let id = media.id, let mediaProgress = pendingStatus.progress.mediaProgress[id] {
|
||||
progress = mediaProgress
|
||||
}
|
||||
let adjustedProgress = max(progress, 0.027)
|
||||
return (.Fetching(isActive: pendingStatus.isRunning, progress: adjustedProgress), resourceStatus)
|
||||
} else {
|
||||
return (resourceStatus, nil)
|
||||
|
|
@ -1472,7 +1510,11 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
updatedStatusSignal = combineLatest(messageMediaFileStatus(context: context, messageId: message.id, file: file, adjustForVideoThumbnail: true), context.account.pendingMessageManager.pendingMessageStatus(message.id) |> map { $0.0 })
|
||||
|> map { resourceStatus, pendingStatus -> (MediaResourceStatus, MediaResourceStatus?) in
|
||||
if let pendingStatus = pendingStatus {
|
||||
let adjustedProgress = max(pendingStatus.progress, 0.027)
|
||||
var progress: Float = pendingStatus.progress.progress
|
||||
if let id = media.id, let mediaProgress = pendingStatus.progress.mediaProgress[id] {
|
||||
progress = mediaProgress
|
||||
}
|
||||
let adjustedProgress = max(progress, 0.027)
|
||||
return (.Fetching(isActive: pendingStatus.isRunning, progress: adjustedProgress), resourceStatus)
|
||||
} else {
|
||||
return (resourceStatus, nil)
|
||||
|
|
@ -1504,6 +1546,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
strongSelf.message = message
|
||||
strongSelf.attributes = attributes
|
||||
strongSelf.media = media
|
||||
strongSelf.mediaIndex = mediaIndex
|
||||
strongSelf.wideLayout = wideLayout
|
||||
strongSelf.themeAndStrings = (presentationData.theme.theme, presentationData.strings, dateTimeFormat.decimalSeparator, presentationData.isPreview)
|
||||
strongSelf.sizeCalculation = sizeCalculation
|
||||
|
|
@ -1743,7 +1786,16 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
|
||||
|
||||
var media = media
|
||||
if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia {
|
||||
var extendedMedia: TelegramExtendedMedia?
|
||||
if let invoice = media as? TelegramMediaInvoice, let selectedMedia = invoice.extendedMedia {
|
||||
extendedMedia = selectedMedia
|
||||
} else if let paidContent = media as? TelegramMediaPaidContent {
|
||||
let selectedMediaIndex = mediaIndex ?? 0
|
||||
if selectedMediaIndex < paidContent.extendedMedia.count {
|
||||
extendedMedia = paidContent.extendedMedia[selectedMediaIndex]
|
||||
}
|
||||
}
|
||||
if let extendedMedia, case let .full(fullMedia) = extendedMedia {
|
||||
media = fullMedia
|
||||
}
|
||||
if let storyMedia = media as? TelegramMediaStory, let storyItem = message.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) {
|
||||
|
|
@ -1848,11 +1900,14 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
var game: TelegramMediaGame?
|
||||
var webpage: TelegramMediaWebpage?
|
||||
var invoice: TelegramMediaInvoice?
|
||||
var paidContent: TelegramMediaPaidContent?
|
||||
for media in message.media {
|
||||
if let media = media as? TelegramMediaWebpage {
|
||||
webpage = media
|
||||
} else if let media = media as? TelegramMediaInvoice {
|
||||
invoice = media
|
||||
} else if let media = media as? TelegramMediaPaidContent {
|
||||
paidContent = media
|
||||
} else if let media = media as? TelegramMediaGame {
|
||||
game = media
|
||||
} else if let _ = media as? TelegramMediaStory {
|
||||
|
|
@ -1860,9 +1915,6 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
automaticPlayback = false
|
||||
}
|
||||
}
|
||||
if let media = self.media as? TelegramMediaInvoice {
|
||||
invoice = media
|
||||
}
|
||||
|
||||
var progressRequired = false
|
||||
if let updatingMedia = attributes.updatingMedia, case .update = updatingMedia.media {
|
||||
|
|
@ -2023,7 +2075,16 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
let formatting = DataSizeStringFormatting(strings: strings, decimalSeparator: decimalSeparator)
|
||||
|
||||
var media = self.media
|
||||
if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia {
|
||||
var extendedMedia: TelegramExtendedMedia?
|
||||
if let invoice = media as? TelegramMediaInvoice, let selectedMedia = invoice.extendedMedia {
|
||||
extendedMedia = selectedMedia
|
||||
} else if let paidContent = media as? TelegramMediaPaidContent {
|
||||
let selectedMediaIndex = mediaIndex ?? 0
|
||||
if selectedMediaIndex < paidContent.extendedMedia.count {
|
||||
extendedMedia = paidContent.extendedMedia[selectedMediaIndex]
|
||||
}
|
||||
}
|
||||
if let extendedMedia, case let .full(fullMedia) = extendedMedia {
|
||||
media = fullMedia
|
||||
}
|
||||
if let storyMedia = media as? TelegramMediaStory, let storyItem = message.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) {
|
||||
|
|
@ -2287,11 +2348,22 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
badgeNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
var icon: ExtendedMediaOverlayNode.Icon? = .lock
|
||||
var icon: ExtendedMediaOverlayNode.Icon?
|
||||
var displaySpoiler = false
|
||||
if let invoice = invoice, let extendedMedia = invoice.extendedMedia, case .preview = extendedMedia {
|
||||
if invoice.currency == "XTR" {
|
||||
icon = nil
|
||||
|
||||
var extendedMedia: TelegramExtendedMedia?
|
||||
if let invoice, let selectedMedia = invoice.extendedMedia {
|
||||
extendedMedia = selectedMedia
|
||||
} else if let paidContent {
|
||||
let selectedMediaIndex = self.mediaIndex ?? 0
|
||||
if selectedMediaIndex < paidContent.extendedMedia.count {
|
||||
extendedMedia = paidContent.extendedMedia[selectedMediaIndex]
|
||||
}
|
||||
}
|
||||
|
||||
if let extendedMedia, case .preview = extendedMedia {
|
||||
if let invoice, invoice.currency != "XTR" {
|
||||
icon = .lock
|
||||
}
|
||||
displaySpoiler = true
|
||||
} else if message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) {
|
||||
|
|
@ -2338,7 +2410,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 {
|
||||
|
|
@ -2355,9 +2427,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
}
|
||||
}
|
||||
}
|
||||
if let invoice, invoice.currency == "XTR" && viewText.isEmpty {
|
||||
viewText = "Unlock for ⭐️\(invoice.totalAmount)"
|
||||
}
|
||||
// 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
|
||||
|
|
@ -2385,12 +2457,12 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
self.extendedMediaOverlayNode?.reveal(animated: true)
|
||||
}
|
||||
|
||||
public static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ peerId: EnginePeer.Id?, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode))) {
|
||||
public static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ mediaIndex: Int?, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ peerId: EnginePeer.Id?, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode))) {
|
||||
let currentAsyncLayout = node?.asyncLayout()
|
||||
|
||||
return { context, presentationData, dateTimeFormat, message, associatedData, attributes, media, dateAndStatus, automaticDownload, peerType, peerId, sizeCalculation, layoutConstants, contentMode, presentationContext in
|
||||
return { context, presentationData, dateTimeFormat, message, associatedData, attributes, media, mediaIndex, dateAndStatus, automaticDownload, peerType, peerId, sizeCalculation, layoutConstants, contentMode, presentationContext in
|
||||
var imageNode: ChatMessageInteractiveMediaNode
|
||||
var imageLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ peerId: EnginePeer.Id?, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void)))
|
||||
var imageLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ mediaIndex: Int?, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ peerId: EnginePeer.Id?, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void)))
|
||||
|
||||
if let node = node, let currentAsyncLayout = currentAsyncLayout {
|
||||
imageNode = node
|
||||
|
|
@ -2400,7 +2472,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
imageLayout = imageNode.asyncLayout()
|
||||
}
|
||||
|
||||
let (unboundSize, initialWidth, continueLayout) = imageLayout(context, presentationData, dateTimeFormat, message, associatedData, attributes, media, dateAndStatus, automaticDownload, peerType, peerId, sizeCalculation, layoutConstants, contentMode, presentationContext)
|
||||
let (unboundSize, initialWidth, continueLayout) = imageLayout(context, presentationData, dateTimeFormat, message, associatedData, attributes, media, mediaIndex, dateAndStatus, automaticDownload, peerType, peerId, sizeCalculation, layoutConstants, contentMode, presentationContext)
|
||||
|
||||
return (unboundSize, initialWidth, { constrainedSize, automaticPlayback, wideLayout, corners in
|
||||
let (finalWidth, finalLayout) = continueLayout(constrainedSize, automaticPlayback, wideLayout, corners)
|
||||
|
|
@ -2566,12 +2638,12 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
}
|
||||
|
||||
public func ignoreTapActionAtPoint(_ point: CGPoint) -> Bool {
|
||||
if let extendedMediaOverlayNode = self.extendedMediaOverlayNode {
|
||||
let convertedPoint = self.view.convert(point, to: extendedMediaOverlayNode.view)
|
||||
if extendedMediaOverlayNode.buttonNode.frame.contains(convertedPoint) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// if let extendedMediaOverlayNode = self.extendedMediaOverlayNode {
|
||||
// let convertedPoint = self.view.convert(point, to: extendedMediaOverlayNode.view)
|
||||
// if extendedMediaOverlayNode.buttonNode.frame.contains(convertedPoint) {
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -847,7 +847,7 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
|
|||
item.controllerInteraction.activateSwitchInline(peerId, "@\(addressName) \(query)", peerTypes)
|
||||
}
|
||||
case .payment:
|
||||
item.controllerInteraction.openCheckoutOrReceipt(item.message.id)
|
||||
item.controllerInteraction.openCheckoutOrReceipt(item.message.id, nil)
|
||||
case let .urlAuth(url, buttonId):
|
||||
item.controllerInteraction.requestMessageActionUrlAuth(url, .message(id: item.message.id, buttonId: buttonId))
|
||||
case .setupPoll:
|
||||
|
|
@ -881,6 +881,10 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
|
|||
open func openMessageContextMenu() {
|
||||
}
|
||||
|
||||
open func makeProgress() -> Promise<Bool>? {
|
||||
return nil
|
||||
}
|
||||
|
||||
open func targetReactionView(value: MessageReaction.Reaction) -> UIView? {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
private var highlightedState: Bool = false
|
||||
|
||||
private var media: Media?
|
||||
private var mediaIndex: Int?
|
||||
private var automaticPlayback: Bool?
|
||||
|
||||
override public var visibility: ListViewItemNodeVisibility {
|
||||
|
|
@ -55,7 +56,8 @@ 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, progress: self.itemNode?.makeProgress()))
|
||||
}
|
||||
|
||||
self.interactiveImageNode.activateAgeRestrictedMedia = { [weak self] in
|
||||
|
|
@ -97,6 +99,8 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
|
||||
return { item, layoutConstants, preparePosition, selection, constrainedSize, _ in
|
||||
var selectedMedia: Media?
|
||||
var selectedMediaIndex: Int?
|
||||
var extendedMedia: TelegramExtendedMedia?
|
||||
var automaticDownload: InteractiveMediaNodeAutodownloadMode = .none
|
||||
var automaticPlayback: Bool = false
|
||||
var contentMode: InteractiveMediaNodeContentMode = .aspectFit
|
||||
|
|
@ -107,15 +111,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
if selectedMedia == nil {
|
||||
for media in item.message.media {
|
||||
if let telegramImage = media as? TelegramMediaImage {
|
||||
// #if DEBUG
|
||||
// if item.message.text == "#" {
|
||||
// selectedMedia = 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)
|
||||
// } else {
|
||||
// selectedMedia = telegramImage
|
||||
// }
|
||||
// #else
|
||||
selectedMedia = telegramImage
|
||||
// #endif
|
||||
if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramImage) {
|
||||
automaticDownload = .full
|
||||
}
|
||||
|
|
@ -177,38 +173,50 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
contentMode = .aspectFill
|
||||
} else if let invoice = media as? TelegramMediaInvoice {
|
||||
selectedMedia = invoice
|
||||
|
||||
if let extendedMedia = invoice.extendedMedia, case let .full(media) = extendedMedia {
|
||||
if let telegramImage = media as? TelegramMediaImage {
|
||||
if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramImage) {
|
||||
automaticDownload = .full
|
||||
}
|
||||
} else if let telegramFile = media as? TelegramMediaFile {
|
||||
if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramFile) {
|
||||
automaticDownload = .full
|
||||
} else if shouldPredownloadMedia(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, media: telegramFile) {
|
||||
automaticDownload = .prefetch
|
||||
}
|
||||
|
||||
if !item.message.containsSecretMedia {
|
||||
if telegramFile.isAnimated && item.context.sharedContext.energyUsageSettings.autoplayGif {
|
||||
if case .full = automaticDownload {
|
||||
automaticPlayback = true
|
||||
} else {
|
||||
automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil
|
||||
}
|
||||
} else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.context.sharedContext.energyUsageSettings.autoplayVideo {
|
||||
if case .full = automaticDownload {
|
||||
automaticPlayback = true
|
||||
} else {
|
||||
automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil
|
||||
}
|
||||
}
|
||||
}
|
||||
contentMode = .aspectFill
|
||||
extendedMedia = invoice.extendedMedia
|
||||
}
|
||||
else if let paidContent = media as? TelegramMediaPaidContent {
|
||||
selectedMedia = paidContent
|
||||
if case let .mosaic(_, _, index) = preparePosition, let index {
|
||||
extendedMedia = paidContent.extendedMedia[index]
|
||||
selectedMediaIndex = index
|
||||
} else {
|
||||
extendedMedia = paidContent.extendedMedia.first
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = selectedMediaIndex
|
||||
|
||||
if let extendedMedia, case let .full(media) = extendedMedia {
|
||||
if let telegramImage = media as? TelegramMediaImage {
|
||||
if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramImage) {
|
||||
automaticDownload = .full
|
||||
}
|
||||
} else if let telegramFile = media as? TelegramMediaFile {
|
||||
if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramFile) {
|
||||
automaticDownload = .full
|
||||
} else if shouldPredownloadMedia(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, media: telegramFile) {
|
||||
automaticDownload = .prefetch
|
||||
}
|
||||
|
||||
if !item.message.containsSecretMedia {
|
||||
if telegramFile.isAnimated && item.context.sharedContext.energyUsageSettings.autoplayGif {
|
||||
if case .full = automaticDownload {
|
||||
automaticPlayback = true
|
||||
} else {
|
||||
automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil
|
||||
}
|
||||
} else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.context.sharedContext.energyUsageSettings.autoplayVideo {
|
||||
if case .full = automaticDownload {
|
||||
automaticPlayback = true
|
||||
} else {
|
||||
automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil
|
||||
}
|
||||
}
|
||||
}
|
||||
contentMode = .aspectFill
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -350,7 +358,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
)
|
||||
}
|
||||
|
||||
let (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.context, item.presentationData, item.presentationData.dateTimeFormat, item.message, item.associatedData, item.attributes, selectedMedia!, dateAndStatus, automaticDownload, item.associatedData.automaticDownloadPeerType, item.associatedData.automaticDownloadPeerId, sizeCalculation, layoutConstants, contentMode, item.controllerInteraction.presentationContext)
|
||||
let (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.context, item.presentationData, item.presentationData.dateTimeFormat, item.message, item.associatedData, item.attributes, selectedMedia!, selectedMediaIndex, dateAndStatus, automaticDownload, item.associatedData.automaticDownloadPeerType, item.associatedData.automaticDownloadPeerId, sizeCalculation, layoutConstants, contentMode, item.controllerInteraction.presentationContext)
|
||||
|
||||
let forceFullCorners = false
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 7.0, hidesBackground: .emptyWallpaper, forceFullCorners: forceFullCorners, forceAlignment: .none)
|
||||
|
|
@ -386,6 +394,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.media = selectedMedia
|
||||
strongSelf.mediaIndex = selectedMediaIndex
|
||||
strongSelf.automaticPlayback = automaticPlayback
|
||||
|
||||
let imageFrame = CGRect(origin: CGPoint(x: bubbleInsets.left, y: bubbleInsets.top), size: imageSize)
|
||||
|
|
@ -441,6 +450,9 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
if let invoice = currentMedia as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia {
|
||||
currentMedia = fullMedia
|
||||
}
|
||||
if let paidContent = currentMedia as? TelegramMediaPaidContent, case let .full(fullMedia) = paidContent.extendedMedia[self.mediaIndex ?? 0] {
|
||||
currentMedia = fullMedia
|
||||
}
|
||||
if currentMedia.isSemanticallyEqual(to: media) {
|
||||
return self.interactiveImageNode.transitionNode(adjustRect: adjustRect)
|
||||
}
|
||||
|
|
@ -455,7 +467,9 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
if let invoice = currentMedia as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia {
|
||||
currentMedia = fullMedia
|
||||
}
|
||||
|
||||
if let paidContent = currentMedia as? TelegramMediaPaidContent, case let .full(fullMedia) = paidContent.extendedMedia[self.mediaIndex ?? 0] {
|
||||
currentMedia = fullMedia
|
||||
}
|
||||
if let currentMedia = currentMedia, let media = media {
|
||||
for item in media {
|
||||
if item.isSemanticallyEqual(to: currentMedia) {
|
||||
|
|
|
|||
|
|
@ -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,291 @@
|
|||
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)
|
||||
}
|
||||
|
||||
var offset: CGFloat = 0.0
|
||||
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))
|
||||
offset -= 1.0
|
||||
}
|
||||
|
||||
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 + offset, 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,344 @@
|
|||
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
|
||||
import HierarchyTrackingLayer
|
||||
|
||||
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 loadingView: LoadingEffectView?
|
||||
|
||||
private var pressed = { }
|
||||
|
||||
private var absolutePosition: (CGRect, CGSize)?
|
||||
|
||||
private var currentProgressDisposable: Disposable?
|
||||
|
||||
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.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.currentProgressDisposable?.dispose()
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
self.pressed()
|
||||
}
|
||||
|
||||
public func makeProgress() -> Promise<Bool> {
|
||||
let progress = Promise<Bool>()
|
||||
self.currentProgressDisposable?.dispose()
|
||||
self.currentProgressDisposable = (progress.get()
|
||||
|> distinctUntilChanged
|
||||
|> deliverOnMainQueue).start(next: { [weak self] hasProgress in
|
||||
guard let self, let loadingView = self.loadingView else {
|
||||
return
|
||||
}
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||
if hasProgress {
|
||||
if loadingView.superview == nil {
|
||||
loadingView.alpha = 0.0
|
||||
self.view.addSubview(loadingView)
|
||||
transition.updateAlpha(layer: loadingView.layer, alpha: 1.0)
|
||||
}
|
||||
} else if loadingView.superview != nil {
|
||||
transition.updateAlpha(layer: loadingView.layer, alpha: 0.0, beginWithCurrentState: true, completion: { finished in
|
||||
if finished {
|
||||
loadingView.removeFromSuperview()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
return progress
|
||||
}
|
||||
|
||||
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
|
||||
let text = NSMutableAttributedString(string: arguments.presentationData.strings.Chat_UnlockMedia("⭐️ \(arguments.media.amount)").string, 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)
|
||||
|
||||
let loadingView: LoadingEffectView
|
||||
if let current = node.loadingView {
|
||||
loadingView = current
|
||||
} else {
|
||||
loadingView = LoadingEffectView()
|
||||
node.loadingView = loadingView
|
||||
}
|
||||
loadingView.frame = CGRect(origin: .zero, size: size)
|
||||
loadingView.update(color: UIColor.white, rect: CGRect(origin: .zero, size: size))
|
||||
|
||||
return node
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let shadowImage: UIImage? = {
|
||||
UIImage(named: "Stories/PanelGradient")
|
||||
}()
|
||||
|
||||
public final class LoadingEffectView: UIView {
|
||||
let hierarchyTrackingLayer: HierarchyTrackingLayer
|
||||
|
||||
private let maskContentsView: UIView
|
||||
private let maskHighlightNode: LinkHighlightingNode
|
||||
|
||||
private let maskBorderContentsView: UIView
|
||||
private let maskBorderHighlightNode: LinkHighlightingNode
|
||||
|
||||
private let backgroundView: UIImageView
|
||||
private let borderBackgroundView: UIImageView
|
||||
|
||||
private var duration: Double
|
||||
private var gradientWidth: CGFloat
|
||||
|
||||
private var inHierarchy = false
|
||||
private var size: CGSize?
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
|
||||
|
||||
self.maskContentsView = UIView()
|
||||
self.maskHighlightNode = LinkHighlightingNode(color: .black)
|
||||
self.maskHighlightNode.useModernPathCalculation = true
|
||||
|
||||
self.maskBorderContentsView = UIView()
|
||||
self.maskBorderHighlightNode = LinkHighlightingNode(color: .black)
|
||||
self.maskBorderHighlightNode.borderOnly = true
|
||||
self.maskBorderHighlightNode.useModernPathCalculation = true
|
||||
|
||||
self.maskBorderContentsView.addSubview(self.maskBorderHighlightNode.view)
|
||||
|
||||
self.backgroundView = UIImageView()
|
||||
self.borderBackgroundView = UIImageView()
|
||||
|
||||
self.gradientWidth = 120.0
|
||||
self.duration = 1.0
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.isUserInteractionEnabled = false
|
||||
|
||||
self.maskContentsView.mask = self.maskHighlightNode.view
|
||||
self.maskContentsView.addSubview(self.backgroundView)
|
||||
self.addSubview(self.maskContentsView)
|
||||
|
||||
self.maskBorderContentsView.mask = self.maskBorderHighlightNode.view
|
||||
self.maskBorderContentsView.addSubview(self.borderBackgroundView)
|
||||
self.addSubview(self.maskBorderContentsView)
|
||||
|
||||
let generateGradient: (CGFloat) -> UIImage? = { baseAlpha in
|
||||
return generateImage(CGSize(width: self.gradientWidth, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let foregroundColor = UIColor(white: 1.0, alpha: min(1.0, baseAlpha * 4.0))
|
||||
|
||||
if let shadowImage {
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
for i in 0 ..< 2 {
|
||||
let shadowFrame = CGRect(origin: CGPoint(x: CGFloat(i) * (size.width * 0.5), y: 0.0), size: CGSize(width: size.width * 0.5, height: size.height))
|
||||
|
||||
context.saveGState()
|
||||
context.translateBy(x: shadowFrame.midX, y: shadowFrame.midY)
|
||||
context.rotate(by: CGFloat(i == 0 ? 1.0 : -1.0) * CGFloat.pi * 0.5)
|
||||
let adjustedRect = CGRect(origin: CGPoint(x: -shadowFrame.height * 0.5, y: -shadowFrame.width * 0.5), size: CGSize(width: shadowFrame.height, height: shadowFrame.width))
|
||||
|
||||
context.clip(to: adjustedRect, mask: shadowImage.cgImage!)
|
||||
context.setFillColor(foregroundColor.cgColor)
|
||||
context.fill(adjustedRect)
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
UIGraphicsPopContext()
|
||||
}
|
||||
})?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
|
||||
self.backgroundView.image = generateGradient(0.5)
|
||||
self.borderBackgroundView.image = generateGradient(1.0)
|
||||
|
||||
self.layer.addSublayer(self.hierarchyTrackingLayer)
|
||||
self.hierarchyTrackingLayer.isInHierarchyUpdated = { [weak self] inHierarchy in
|
||||
guard let self, let size = self.size else {
|
||||
return
|
||||
}
|
||||
self.inHierarchy = inHierarchy
|
||||
self.updateAnimations(size: size)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func updateAnimations(size: CGSize) {
|
||||
if self.inHierarchy {
|
||||
if self.backgroundView.layer.animation(forKey: "shimmer") != nil {
|
||||
return
|
||||
}
|
||||
let animation = self.backgroundView.layer.makeAnimation(from: 0.0 as NSNumber, to: (size.width + self.gradientWidth + size.width * 0.0) as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: self.duration, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
|
||||
animation.repeatCount = Float.infinity
|
||||
self.backgroundView.layer.add(animation, forKey: "shimmer")
|
||||
self.borderBackgroundView.layer.add(animation, forKey: "shimmer")
|
||||
} else {
|
||||
self.backgroundView.layer.removeAllAnimations()
|
||||
self.borderBackgroundView.layer.removeAllAnimations()
|
||||
}
|
||||
}
|
||||
|
||||
public func update(color: UIColor, rect: CGRect) {
|
||||
let maskFrame = CGRect(origin: CGPoint(), size: rect.size).insetBy(dx: -4.0, dy: -4.0)
|
||||
|
||||
self.gradientWidth = 260.0
|
||||
self.duration = 1.2
|
||||
|
||||
self.maskContentsView.backgroundColor = .clear
|
||||
self.maskBorderContentsView.backgroundColor = color.withAlphaComponent(0.12)
|
||||
|
||||
// self.backgroundView.alpha = 0.25
|
||||
self.backgroundView.tintColor = color
|
||||
|
||||
self.borderBackgroundView.tintColor = color
|
||||
|
||||
self.maskContentsView.frame = maskFrame
|
||||
self.maskBorderContentsView.frame = maskFrame
|
||||
|
||||
let rectsSet: [CGRect] = [rect]
|
||||
|
||||
self.maskHighlightNode.outerRadius = rect.height / 2.0
|
||||
self.maskHighlightNode.updateRects(rectsSet)
|
||||
self.maskHighlightNode.frame = CGRect(origin: CGPoint(x: -maskFrame.minX, y: -maskFrame.minY), size: CGSize())
|
||||
|
||||
self.maskBorderHighlightNode.outerRadius = rect.height / 2.0
|
||||
self.maskBorderHighlightNode.updateRects(rectsSet)
|
||||
self.maskBorderHighlightNode.frame = CGRect(origin: CGPoint(x: -maskFrame.minX, y: -maskFrame.minY), size: CGSize())
|
||||
|
||||
if self.size != maskFrame.size {
|
||||
self.size = maskFrame.size
|
||||
|
||||
self.backgroundView.frame = CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: maskFrame.height))
|
||||
self.borderBackgroundView.frame = CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: maskFrame.height))
|
||||
|
||||
self.updateAnimations(size: maskFrame.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -557,7 +557,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||
break
|
||||
}
|
||||
}
|
||||
}, openCheckoutOrReceipt: { _ in
|
||||
}, openCheckoutOrReceipt: { _, _ in
|
||||
}, openSearch: {
|
||||
}, setupReply: { _ in
|
||||
}, canSetupReply: { _ in
|
||||
|
|
|
|||
|
|
@ -428,7 +428,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
|
|||
return nil
|
||||
}, chatControllerNode: {
|
||||
return nil
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _, _ in }, openSearch: { }, setupReply: { _ in
|
||||
}, canSetupReply: { _ in
|
||||
return .none
|
||||
}, canSendMessages: {
|
||||
|
|
@ -619,7 +619,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
|
|||
|
||||
let prepareLayout = messageNode.asyncLayoutContent()
|
||||
|
||||
let prepareContentPosition: ChatMessageBubblePreparePosition = .mosaic(top: .None(.None(.Incoming)), bottom: i == (items.count - 1 - 1) ? bottomPosition : .None(.None(.Incoming)))
|
||||
let prepareContentPosition: ChatMessageBubblePreparePosition = .mosaic(top: .None(.None(.Incoming)), bottom: i == (items.count - 1 - 1) ? bottomPosition : .None(.None(.Incoming)), index: i)
|
||||
|
||||
let (properties, unboundSize, maxNodeWidth, nodeLayout) = prepareLayout(items[i], layoutConstants, prepareContentPosition, nil, CGSize(width: maximumContentWidth, height: CGFloat.greatestFiniteMagnitude), 0.0)
|
||||
maximumNodeWidth = min(maximumNodeWidth, maxNodeWidth)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -156,7 +158,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
|||
public var messageNode: ASDisplayNode?
|
||||
public var progress: Promise<Bool>?
|
||||
|
||||
public init(message: Message?, contentNode: ContextExtractedContentContainingNode? = nil, messageNode: ASDisplayNode? = nil, progress: Promise<Bool>? = nil) {
|
||||
public init(message: Message? = nil, contentNode: ContextExtractedContentContainingNode? = nil, messageNode: ASDisplayNode? = nil, progress: Promise<Bool>? = nil) {
|
||||
self.message = message
|
||||
self.contentNode = contentNode
|
||||
self.messageNode = messageNode
|
||||
|
|
@ -205,7 +207,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
|||
public let presentGlobalOverlayController: (ViewController, Any?) -> Void
|
||||
public let callPeer: (PeerId, Bool) -> Void
|
||||
public let longTap: (ChatControllerInteractionLongTapAction, LongTapParams?) -> Void
|
||||
public let openCheckoutOrReceipt: (MessageId) -> Void
|
||||
public let openCheckoutOrReceipt: (MessageId, OpenMessageParams?) -> Void
|
||||
public let openSearch: () -> Void
|
||||
public let setupReply: (MessageId) -> Void
|
||||
public let canSetupReply: (Message) -> ChatControllerInteractionSwipeAction
|
||||
|
|
@ -334,7 +336,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
|||
presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void,
|
||||
callPeer: @escaping (PeerId, Bool) -> Void,
|
||||
longTap: @escaping (ChatControllerInteractionLongTapAction, LongTapParams?) -> Void,
|
||||
openCheckoutOrReceipt: @escaping (MessageId) -> Void,
|
||||
openCheckoutOrReceipt: @escaping (MessageId, OpenMessageParams?) -> Void,
|
||||
openSearch: @escaping () -> Void,
|
||||
setupReply: @escaping (MessageId) -> Void,
|
||||
canSetupReply: @escaping (Message) -> ChatControllerInteractionSwipeAction,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -5750,14 +5750,15 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||
|
||||
self?.node.presentGallery()
|
||||
})))
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Link", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, a in
|
||||
a(.default)
|
||||
|
||||
self?.node.addOrEditLink()
|
||||
})))
|
||||
if self.context.isPremium {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaEditor_Shortcut_Link, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, a in
|
||||
a(.default)
|
||||
|
||||
self?.node.addOrEditLink()
|
||||
})))
|
||||
}
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaEditor_Shortcut_Location, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/LocationSmall"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, a in
|
||||
|
|
|
|||
|
|
@ -3295,7 +3295,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||
default:
|
||||
break
|
||||
}
|
||||
}, openCheckoutOrReceipt: { _ in
|
||||
}, openCheckoutOrReceipt: { _, _ in
|
||||
}, openSearch: {
|
||||
}, setupReply: { _ in
|
||||
}, canSetupReply: { _ in
|
||||
|
|
|
|||
|
|
@ -384,6 +384,10 @@ public final class GiftAvatarComponent: Component {
|
|||
iconBackgroundView.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1b1f24))
|
||||
iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment")
|
||||
iconOffset = 5.0
|
||||
case .ads:
|
||||
iconBackgroundView.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1b1f24))
|
||||
iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment")
|
||||
iconOffset = 5.0
|
||||
case .premiumBot:
|
||||
iconInset = 15.0
|
||||
iconBackgroundView.image = generateGradientFilledCircleImage(
|
||||
|
|
|
|||
|
|
@ -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,362 @@
|
|||
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]
|
||||
let backgroundColor: UIColor
|
||||
|
||||
public init(context: AccountContext, theme: PresentationTheme, peer: StarsContext.State.Transaction.Peer, photo: TelegramMediaWebFile?, media: [Media], backgroundColor: UIColor) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.peer = peer
|
||||
self.photo = photo
|
||||
self.media = media
|
||||
self.backgroundColor = backgroundColor
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if lhs.backgroundColor != rhs.backgroundColor {
|
||||
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 var imageFrameNode: UIView?
|
||||
private var secondImageNode: TransformImageNode?
|
||||
|
||||
private let fetchDisposable = DisposableSet()
|
||||
|
||||
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
|
||||
var isFirstTime = false
|
||||
if let current = self.imageNode {
|
||||
imageNode = current
|
||||
} else {
|
||||
isFirstTime = true
|
||||
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)
|
||||
}
|
||||
if isFirstTime {
|
||||
imageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false))
|
||||
self.fetchDisposable.add(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)
|
||||
}
|
||||
if isFirstTime {
|
||||
imageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file), autoFetchFullSizeThumbnail: true))
|
||||
}
|
||||
}
|
||||
|
||||
var imageFrame = CGRect(origin: .zero, size: size)
|
||||
if component.media.count > 1 {
|
||||
imageFrame = imageFrame.insetBy(dx: 2.0, dy: 2.0).offsetBy(dx: -2.0, dy: 2.0)
|
||||
}
|
||||
imageNode.frame = imageFrame
|
||||
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 8.0), imageSize: dimensions, boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
|
||||
|
||||
if component.media.count > 1 {
|
||||
let secondImageNode: TransformImageNode
|
||||
let imageFrameNode: UIView
|
||||
var secondDimensions = size
|
||||
var isFirstTime = false
|
||||
if let current = self.secondImageNode, let currentFrame = self.imageFrameNode {
|
||||
secondImageNode = current
|
||||
imageFrameNode = currentFrame
|
||||
} else {
|
||||
isFirstTime = true
|
||||
secondImageNode = TransformImageNode()
|
||||
secondImageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
|
||||
self.insertSubview(secondImageNode.view, belowSubview: imageNode.view)
|
||||
self.secondImageNode = secondImageNode
|
||||
|
||||
imageFrameNode = UIView()
|
||||
imageFrameNode.layer.cornerRadius = 8.0
|
||||
self.insertSubview(imageFrameNode, belowSubview: imageNode.view)
|
||||
self.imageFrameNode = imageFrameNode
|
||||
}
|
||||
|
||||
if let image = component.media[1] as? TelegramMediaImage {
|
||||
if let imageDimensions = largestImageRepresentation(image.representations)?.dimensions {
|
||||
secondDimensions = imageDimensions.cgSize.aspectFilled(size)
|
||||
}
|
||||
if isFirstTime {
|
||||
secondImageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false))
|
||||
self.fetchDisposable.add(chatMessagePhotoInteractiveFetched(context: component.context, userLocation: .other, photoReference: .standalone(media: image), displayAtSize: nil, storeToDownloadsPeerId: nil).startStrict())
|
||||
}
|
||||
} else if let file = component.media[1] as? TelegramMediaFile {
|
||||
if let videoDimensions = file.dimensions {
|
||||
secondDimensions = videoDimensions.cgSize.aspectFilled(size)
|
||||
}
|
||||
if isFirstTime {
|
||||
secondImageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file), useLargeThumbnail: true, autoFetchFullSizeThumbnail: true))
|
||||
}
|
||||
}
|
||||
|
||||
imageFrameNode.backgroundColor = component.backgroundColor
|
||||
secondImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 8.0), imageSize: secondDimensions, boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
|
||||
secondImageNode.frame = imageFrame.offsetBy(dx: 4.0, dy: -4.0)
|
||||
imageFrameNode.frame = imageFrame.insetBy(dx: -1.0 - UIScreenPixel, dy: -1.0 - UIScreenPixel)
|
||||
}
|
||||
|
||||
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.add(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 .ads:
|
||||
self.backgroundView.image = generateGradientFilledCircleImage(
|
||||
diameter: size.width,
|
||||
colors: [
|
||||
UIColor(rgb: 0xffa85c).cgColor,
|
||||
UIColor(rgb: 0xffcd6a).cgColor
|
||||
],
|
||||
direction: .mirroredDiagonal
|
||||
)
|
||||
self.backgroundView.isHidden = false
|
||||
self.iconView.isHidden = false
|
||||
self.avatarNode.isHidden = true
|
||||
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
|
|
@ -247,25 +248,67 @@ 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 let action: ((@escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
subject: Subject,
|
||||
theme: PresentationTheme,
|
||||
diameter: CGFloat
|
||||
diameter: CGFloat,
|
||||
backgroundColor: UIColor,
|
||||
action: ((@escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? = nil
|
||||
) {
|
||||
self.context = context
|
||||
self.subject = subject
|
||||
self.theme = theme
|
||||
self.diameter = diameter
|
||||
self.backgroundColor = backgroundColor
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public static func ==(lhs: StarsImageComponent, rhs: StarsImageComponent) -> Bool {
|
||||
|
|
@ -275,25 +318,41 @@ 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
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var component: StarsImageComponent?
|
||||
private var state: EmptyComponentState?
|
||||
|
||||
private var smallParticlesView: StarsParticlesView?
|
||||
private var largeParticlesView: StarsParticlesView?
|
||||
|
||||
private var containerNode: ASDisplayNode?
|
||||
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 button: UIControl?
|
||||
|
||||
private var countView = ComponentView<Empty>()
|
||||
|
||||
private let fetchDisposable = MetaDisposable()
|
||||
private var hiddenMediaDisposable: Disposable?
|
||||
|
||||
private var hiddenMedia: [Media] = []
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
|
@ -304,10 +363,41 @@ public final class StarsImageComponent: Component {
|
|||
}
|
||||
deinit {
|
||||
self.fetchDisposable.dispose()
|
||||
self.hiddenMediaDisposable?.dispose()
|
||||
}
|
||||
|
||||
func update(component: StarsImageComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
|
||||
@objc private func buttonPressed() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.action?({ [weak self] media in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
return self.transitionNode(media)
|
||||
}, { [weak self] view in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.superview?.addSubview(view)
|
||||
})
|
||||
}
|
||||
|
||||
public func transitionNode(_ transitionMedia: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
||||
guard let component = self.component, let containerNode = self.containerNode else {
|
||||
return nil
|
||||
}
|
||||
if case let .media(media) = component.subject, media.first?.id == transitionMedia.id {
|
||||
return (containerNode, containerNode.bounds, { [weak containerNode] in
|
||||
return (containerNode?.view.snapshotContentTree(unhide: true), nil)
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func update(component: StarsImageComponent, state: EmptyComponentState, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let smallParticlesView: StarsParticlesView
|
||||
if let current = self.smallParticlesView {
|
||||
|
|
@ -333,8 +423,26 @@ public final class StarsImageComponent: Component {
|
|||
largeParticlesView.update(size: availableSize)
|
||||
largeParticlesView.frame = CGRect(origin: .zero, size: availableSize)
|
||||
|
||||
let imageSize = CGSize(width: component.diameter, height: component.diameter)
|
||||
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - imageSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - imageSize.height) / 2.0)), size: imageSize)
|
||||
let containerNode: ASDisplayNode
|
||||
if let current = self.containerNode {
|
||||
containerNode = current
|
||||
} else {
|
||||
containerNode = ASDisplayNode()
|
||||
|
||||
self.addSubview(containerNode.view)
|
||||
self.containerNode = containerNode
|
||||
}
|
||||
|
||||
var imageSize = CGSize(width: component.diameter, height: component.diameter)
|
||||
let containerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - imageSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - imageSize.height) / 2.0)), size: imageSize)
|
||||
containerNode.frame = containerFrame
|
||||
|
||||
if case let .media(media) = component.subject, media.count > 1 {
|
||||
imageSize = CGSize(width: component.diameter - 6.0, height: component.diameter - 6.0)
|
||||
} else if case let .extendedMedia(media) = component.subject, media.count > 1 {
|
||||
imageSize = CGSize(width: component.diameter - 6.0, height: component.diameter - 6.0)
|
||||
}
|
||||
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((containerFrame.width - imageSize.width) / 2.0), y: floorToScreenPixels((containerFrame.height - imageSize.height) / 2.0)), size: imageSize)
|
||||
|
||||
switch component.subject {
|
||||
case .none:
|
||||
|
|
@ -346,7 +454,7 @@ public final class StarsImageComponent: Component {
|
|||
} else {
|
||||
imageNode = TransformImageNode()
|
||||
imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
|
||||
self.addSubview(imageNode.view)
|
||||
containerNode.view.addSubview(imageNode.view)
|
||||
self.imageNode = imageNode
|
||||
|
||||
imageNode.setSignal(chatWebFileImage(account: component.context.account, file: photo))
|
||||
|
|
@ -355,39 +463,201 @@ 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
|
||||
var isFirstTime = false
|
||||
if let current = self.imageNode {
|
||||
imageNode = current
|
||||
} else {
|
||||
isFirstTime = true
|
||||
imageNode = TransformImageNode()
|
||||
imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
|
||||
containerNode.view.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)
|
||||
}
|
||||
if isFirstTime {
|
||||
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)
|
||||
}
|
||||
if isFirstTime {
|
||||
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 let firstMedia = media.first, self.hiddenMedia.contains(where: { $0.id == firstMedia.id }) {
|
||||
containerNode.isHidden = true
|
||||
} else {
|
||||
containerNode.isHidden = false
|
||||
}
|
||||
|
||||
if media.count > 1 {
|
||||
let secondImageNode: TransformImageNode
|
||||
let imageFrameNode: UIView
|
||||
var secondDimensions = imageSize
|
||||
if let current = self.secondImageNode, let currentFrame = self.imageFrameNode {
|
||||
secondImageNode = current
|
||||
imageFrameNode = currentFrame
|
||||
} else {
|
||||
secondImageNode = TransformImageNode()
|
||||
secondImageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
|
||||
containerNode.view.insertSubview(secondImageNode.view, belowSubview: imageNode.view)
|
||||
self.secondImageNode = secondImageNode
|
||||
|
||||
imageFrameNode = UIView()
|
||||
imageFrameNode.layer.cornerRadius = 17.0
|
||||
containerNode.view.insertSubview(imageFrameNode, belowSubview: imageNode.view)
|
||||
self.imageFrameNode = imageFrameNode
|
||||
}
|
||||
|
||||
if let image = media[1] as? TelegramMediaImage {
|
||||
if let imageDimensions = largestImageRepresentation(image.representations)?.dimensions {
|
||||
secondDimensions = imageDimensions.cgSize.aspectFilled(imageSize)
|
||||
}
|
||||
if isFirstTime {
|
||||
secondImageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false))
|
||||
}
|
||||
} else if let file = media[1] as? TelegramMediaFile {
|
||||
if let videoDimensions = file.dimensions {
|
||||
secondDimensions = videoDimensions.cgSize.aspectFilled(imageSize)
|
||||
}
|
||||
if isFirstTime {
|
||||
secondImageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file), useLargeThumbnail: true, autoFetchFullSizeThumbnail: true))
|
||||
}
|
||||
}
|
||||
|
||||
imageFrameNode.backgroundColor = component.backgroundColor
|
||||
secondImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: secondDimensions, 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 {
|
||||
containerNode.view.addSubview(countView)
|
||||
}
|
||||
countView.frame = countFrame
|
||||
}
|
||||
}
|
||||
case let .extendedMedia(extendedMedia):
|
||||
let imageNode: TransformImageNode
|
||||
let dustNode: MediaDustNode
|
||||
var dimensions = imageSize
|
||||
var isFirstTime = false
|
||||
if let current = self.imageNode, let currentDust = self.dustNode {
|
||||
imageNode = current
|
||||
dustNode = currentDust
|
||||
} else {
|
||||
isFirstTime = true
|
||||
imageNode = TransformImageNode()
|
||||
imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
|
||||
self.addSubview(imageNode.view)
|
||||
containerNode.view.addSubview(imageNode.view)
|
||||
self.imageNode = imageNode
|
||||
|
||||
let media: TelegramMediaImage
|
||||
switch extendedMedia {
|
||||
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
|
||||
}
|
||||
|
||||
imageNode.setSignal(chatSecretPhoto(account: component.context.account, userLocation: .other, photoReference: .standalone(media: media), ignoreFullSize: true, synchronousLoad: true))
|
||||
|
||||
|
||||
dustNode = MediaDustNode(enableAnimations: true)
|
||||
self.addSubview(dustNode.view)
|
||||
dustNode.isUserInteractionEnabled = false
|
||||
containerNode.view.addSubview(dustNode.view)
|
||||
self.dustNode = dustNode
|
||||
}
|
||||
|
||||
|
||||
let media: TelegramMediaImage
|
||||
switch extendedMedia.first {
|
||||
case let .preview(imageDimensions, immediateThumbnailData, _):
|
||||
let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
||||
media = thumbnailMedia
|
||||
if let imageDimensions {
|
||||
dimensions = imageDimensions.cgSize.aspectFilled(imageSize)
|
||||
}
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
if isFirstTime {
|
||||
imageNode.setSignal(chatSecretPhoto(account: component.context.account, userLocation: .other, photoReference: .standalone(media: media), ignoreFullSize: true, synchronousLoad: true))
|
||||
}
|
||||
|
||||
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: dimensions, 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 secondImageNode: TransformImageNode
|
||||
let imageFrameNode: UIView
|
||||
var secondDimensions = imageSize
|
||||
var isFirstTime = false
|
||||
if let current = self.secondImageNode, let currentFrame = self.imageFrameNode {
|
||||
secondImageNode = current
|
||||
imageFrameNode = currentFrame
|
||||
} else {
|
||||
isFirstTime = true
|
||||
secondImageNode = TransformImageNode()
|
||||
secondImageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
|
||||
containerNode.view.insertSubview(secondImageNode.view, belowSubview: imageNode.view)
|
||||
self.secondImageNode = secondImageNode
|
||||
|
||||
imageFrameNode = UIView()
|
||||
imageFrameNode.layer.cornerRadius = 17.0
|
||||
containerNode.view.insertSubview(imageFrameNode, belowSubview: imageNode.view)
|
||||
self.imageFrameNode = imageFrameNode
|
||||
}
|
||||
|
||||
let media: TelegramMediaImage
|
||||
switch extendedMedia[1] {
|
||||
case let .preview(imageDimensions, immediateThumbnailData, _):
|
||||
let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
||||
media = thumbnailMedia
|
||||
if let imageDimensions {
|
||||
secondDimensions = imageDimensions.cgSize.aspectFilled(imageSize)
|
||||
}
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
|
||||
if isFirstTime {
|
||||
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: secondDimensions, 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)
|
||||
}
|
||||
|
||||
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 {
|
||||
containerNode.view.addSubview(countView)
|
||||
}
|
||||
countView.frame = countFrame
|
||||
}
|
||||
}
|
||||
case let .transactionPeer(peer):
|
||||
if case let .peer(peer) = peer {
|
||||
let avatarNode: ImageNode
|
||||
|
|
@ -396,7 +666,7 @@ public final class StarsImageComponent: Component {
|
|||
} else {
|
||||
avatarNode = ImageNode()
|
||||
avatarNode.displaysAsynchronously = false
|
||||
self.addSubview(avatarNode.view)
|
||||
containerNode.view.addSubview(avatarNode.view)
|
||||
self.avatarNode = avatarNode
|
||||
|
||||
avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: peer, size: imageSize, font: avatarPlaceholderFont(size: 43.0), fullSize: true))
|
||||
|
|
@ -412,8 +682,8 @@ public final class StarsImageComponent: Component {
|
|||
iconBackgroundView = UIImageView()
|
||||
iconView = UIImageView()
|
||||
|
||||
self.addSubview(iconBackgroundView)
|
||||
self.addSubview(iconView)
|
||||
containerNode.view.addSubview(iconBackgroundView)
|
||||
containerNode.view.addSubview(iconView)
|
||||
|
||||
self.iconBackgroundView = iconBackgroundView
|
||||
self.iconView = iconView
|
||||
|
|
@ -449,6 +719,16 @@ public final class StarsImageComponent: Component {
|
|||
)
|
||||
iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment")
|
||||
iconOffset = 5.0
|
||||
case .ads:
|
||||
iconBackgroundView.image = generateGradientFilledCircleImage(
|
||||
diameter: imageSize.width,
|
||||
colors: [
|
||||
UIColor(rgb: 0xffa85c).cgColor,
|
||||
UIColor(rgb: 0xffcd6a).cgColor
|
||||
],
|
||||
direction: .mirroredDiagonal
|
||||
)
|
||||
iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white)
|
||||
case .premiumBot:
|
||||
iconInset = 15.0
|
||||
iconBackgroundView.image = generateGradientFilledCircleImage(
|
||||
|
|
@ -479,6 +759,40 @@ public final class StarsImageComponent: Component {
|
|||
iconView.frame = imageFrame.insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset)
|
||||
}
|
||||
}
|
||||
|
||||
if let _ = component.action {
|
||||
if self.button == nil {
|
||||
let button = UIControl(frame: imageFrame)
|
||||
button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
|
||||
containerNode.view.addSubview(button)
|
||||
self.button = button
|
||||
}
|
||||
} else if let button = self.button {
|
||||
self.button = nil
|
||||
button.removeFromSuperview()
|
||||
}
|
||||
|
||||
if case .media = component.subject {
|
||||
if self.hiddenMediaDisposable == nil {
|
||||
self.hiddenMediaDisposable = component.context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().startStrict(next: { [weak self] ids in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
var hiddenMedia: [Media] = []
|
||||
for id in ids {
|
||||
if case let .chat(accountId, _, media) = id, accountId == component.context.account.id {
|
||||
hiddenMedia.append(media)
|
||||
}
|
||||
}
|
||||
self.hiddenMedia = hiddenMedia
|
||||
self.state?.updated()
|
||||
}).strict()
|
||||
}
|
||||
} else if let hiddenMediaDisposable = self.hiddenMediaDisposable {
|
||||
self.hiddenMediaDisposable = nil
|
||||
hiddenMediaDisposable.dispose()
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
|
@ -488,6 +802,6 @@ public final class StarsImageComponent: Component {
|
|||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
return view.update(component: self, state: state, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
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",
|
||||
"//submodules/GalleryUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -2,6 +2,7 @@ import Foundation
|
|||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
|
|
@ -20,6 +21,7 @@ import TextFormat
|
|||
import TelegramStringFormatting
|
||||
import UndoUI
|
||||
import StarsImageComponent
|
||||
import GalleryUI
|
||||
|
||||
private final class StarsTransactionSheetContent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
|
@ -29,7 +31,9 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
let action: () -> Void
|
||||
let cancel: (Bool) -> Void
|
||||
let openPeer: (EnginePeer) -> Void
|
||||
let copyTransactionId: () -> Void
|
||||
let openMessage: (EngineMessage.Id) -> Void
|
||||
let openMedia: ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void
|
||||
let copyTransactionId: (String) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
|
|
@ -37,13 +41,17 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
action: @escaping () -> Void,
|
||||
cancel: @escaping (Bool) -> Void,
|
||||
openPeer: @escaping (EnginePeer) -> Void,
|
||||
copyTransactionId: @escaping () -> Void
|
||||
openMessage: @escaping (EngineMessage.Id) -> Void,
|
||||
openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void,
|
||||
copyTransactionId: @escaping (String) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.subject = subject
|
||||
self.action = action
|
||||
self.cancel = cancel
|
||||
self.openPeer = openPeer
|
||||
self.openMessage = openMessage
|
||||
self.openMedia = openMedia
|
||||
self.copyTransactionId = copyTransactionId
|
||||
}
|
||||
|
||||
|
|
@ -172,17 +180,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,18 +208,52 @@ 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 {
|
||||
titleText = strings.Stars_Transaction_FragmentWithdrawal_Title
|
||||
via = strings.Stars_Transaction_FragmentWithdrawal_Subtitle
|
||||
}
|
||||
case .ads:
|
||||
titleText = strings.Stars_Transaction_TelegramAds_Title
|
||||
via = strings.Stars_Transaction_TelegramAds_Subtitle
|
||||
case .unsupported:
|
||||
titleText = strings.Stars_Transaction_Unsupported_Title
|
||||
via = nil
|
||||
}
|
||||
descriptionText = transaction.description ?? ""
|
||||
if !transaction.media.isEmpty {
|
||||
var description: String = ""
|
||||
var photoCount: Int32 = 0
|
||||
var videoCount: Int32 = 0
|
||||
for media in transaction.media {
|
||||
if let _ = media as? TelegramMediaFile {
|
||||
videoCount += 1
|
||||
} else {
|
||||
photoCount += 1
|
||||
}
|
||||
}
|
||||
if photoCount > 0 && videoCount > 0 {
|
||||
description += strings.Stars_Transaction_MediaAnd(strings.Stars_Transaction_Photos(photoCount), strings.Stars_Transaction_Videos(videoCount)).string
|
||||
} else if photoCount > 0 {
|
||||
if photoCount > 1 {
|
||||
description += strings.Stars_Transaction_Photos(photoCount)
|
||||
} else {
|
||||
description += strings.Stars_Transaction_SinglePhoto
|
||||
}
|
||||
} else if videoCount > 0 {
|
||||
if videoCount > 1 {
|
||||
description += strings.Stars_Transaction_Videos(videoCount)
|
||||
} else {
|
||||
description += strings.Stars_Transaction_SingleVideo
|
||||
}
|
||||
}
|
||||
descriptionText = description
|
||||
} else {
|
||||
descriptionText = transaction.description ?? ""
|
||||
}
|
||||
|
||||
messageId = transaction.paidMessageId
|
||||
|
||||
count = transaction.count
|
||||
transactionId = transaction.id
|
||||
|
|
@ -216,6 +264,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 +272,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 +281,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
toPeer = nil
|
||||
}
|
||||
transactionPeer = nil
|
||||
media = []
|
||||
photo = receipt.invoiceMedia.photo
|
||||
isRefund = false
|
||||
delayedCloseOnOpenPeer = false
|
||||
|
|
@ -261,7 +312,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 +328,11 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
context: component.context,
|
||||
subject: imageSubject,
|
||||
theme: theme,
|
||||
diameter: 90.0
|
||||
diameter: 90.0,
|
||||
backgroundColor: theme.actionSheet.opaqueItemBackgroundColor,
|
||||
action: !media.isEmpty ? { transitionNode, addToTransitionSurface in
|
||||
component.openMedia(media, transitionNode, addToTransitionSurface)
|
||||
} : nil
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: 200.0),
|
||||
transition: .immediate
|
||||
|
|
@ -345,6 +402,40 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
)
|
||||
))
|
||||
}
|
||||
|
||||
if let messageId {
|
||||
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: "media",
|
||||
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(
|
||||
|
|
@ -360,7 +451,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
)
|
||||
),
|
||||
action: {
|
||||
component.copyTransactionId()
|
||||
component.copyTransactionId(transactionId)
|
||||
}
|
||||
)
|
||||
),
|
||||
|
|
@ -544,19 +635,25 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
|
|||
let subject: StarsTransactionScreen.Subject
|
||||
let action: () -> Void
|
||||
let openPeer: (EnginePeer) -> Void
|
||||
let copyTransactionId: () -> Void
|
||||
let openMessage: (EngineMessage.Id) -> Void
|
||||
let openMedia: ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void
|
||||
let copyTransactionId: (String) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
subject: StarsTransactionScreen.Subject,
|
||||
action: @escaping () -> Void,
|
||||
openPeer: @escaping (EnginePeer) -> Void,
|
||||
copyTransactionId: @escaping () -> Void
|
||||
openMessage: @escaping (EngineMessage.Id) -> Void,
|
||||
openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void,
|
||||
copyTransactionId: @escaping (String) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.subject = subject
|
||||
self.action = action
|
||||
self.openPeer = openPeer
|
||||
self.openMessage = openMessage
|
||||
self.openMedia = openMedia
|
||||
self.copyTransactionId = copyTransactionId
|
||||
}
|
||||
|
||||
|
|
@ -599,6 +696,8 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
|
|||
}
|
||||
},
|
||||
openPeer: context.component.openPeer,
|
||||
openMessage: context.component.openMessage,
|
||||
openMedia: context.component.openMedia,
|
||||
copyTransactionId: context.component.copyTransactionId
|
||||
)),
|
||||
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
|
||||
|
|
@ -667,7 +766,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,7 +784,9 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
|
|||
self.context = context
|
||||
|
||||
var openPeerImpl: ((EnginePeer) -> Void)?
|
||||
var copyTransactionIdImpl: (() -> Void)?
|
||||
var openMessageImpl: ((EngineMessage.Id) -> Void)?
|
||||
var openMediaImpl: (([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)?
|
||||
var copyTransactionIdImpl: ((String) -> Void)?
|
||||
super.init(
|
||||
context: context,
|
||||
component: StarsTransactionSheetComponent(
|
||||
|
|
@ -695,8 +796,14 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
|
|||
openPeer: { peerId in
|
||||
openPeerImpl?(peerId)
|
||||
},
|
||||
copyTransactionId: {
|
||||
copyTransactionIdImpl?()
|
||||
openMessage: { messageId in
|
||||
openMessageImpl?(messageId)
|
||||
},
|
||||
openMedia: { media, transitionNode, addToTransitionSurface in
|
||||
openMediaImpl?(media, transitionNode, addToTransitionSurface)
|
||||
},
|
||||
copyTransactionId: { transactionId in
|
||||
copyTransactionIdImpl?(transactionId)
|
||||
}
|
||||
),
|
||||
navigationBarAppearance: .none,
|
||||
|
|
@ -724,10 +831,70 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
|
|||
})
|
||||
}
|
||||
|
||||
copyTransactionIdImpl = { [weak self] in
|
||||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
openMediaImpl = { [weak self] media, transitionNode, addToTransitionSurface in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let message = Message(
|
||||
stableId: 0,
|
||||
stableVersion: 0,
|
||||
id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(0)), namespace: Namespaces.Message.Local, id: 0),
|
||||
globallyUniqueId: 0,
|
||||
groupingKey: nil,
|
||||
groupInfo: nil,
|
||||
threadId: nil,
|
||||
timestamp: 0,
|
||||
flags: [],
|
||||
tags: [],
|
||||
globalTags: [],
|
||||
localTags: [],
|
||||
customTags: [],
|
||||
forwardInfo: nil,
|
||||
author: nil,
|
||||
text: "",
|
||||
attributes: [],
|
||||
media: [TelegramMediaPaidContent(amount: 0, extendedMedia: media.map { .full(media: $0) })],
|
||||
peers: SimpleDictionary(),
|
||||
associatedMessages: SimpleDictionary(),
|
||||
associatedMessageIds: [],
|
||||
associatedMedia: [:],
|
||||
associatedThreadInfo: nil,
|
||||
associatedStories: [:]
|
||||
)
|
||||
let gallery = GalleryController(context: self.context, source: .standaloneMessage(message, 0), replaceRootController: { _, _ in
|
||||
}, baseNavigationController: nil)
|
||||
self.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { messageId, media in
|
||||
if let transitionNode = transitionNode(media) {
|
||||
return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: addToTransitionSurface)
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
|
||||
copyTransactionIdImpl = { [weak self] transactionId in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
UIPasteboard.general.string = transactionId
|
||||
|
||||
self.dismissAllTooltips()
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
|
@ -1210,3 +1377,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",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ final class StarsBalanceComponent: Component {
|
|||
let actionAvailable: Bool
|
||||
let actionIsEnabled: Bool
|
||||
let actionCooldownUntilTimestamp: Int32?
|
||||
let buy: () -> Void
|
||||
let action: () -> Void
|
||||
let buyAds: (() -> Void)?
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
|
|
@ -33,7 +34,8 @@ final class StarsBalanceComponent: Component {
|
|||
actionAvailable: Bool,
|
||||
actionIsEnabled: Bool,
|
||||
actionCooldownUntilTimestamp: Int32? = nil,
|
||||
buy: @escaping () -> Void
|
||||
action: @escaping () -> Void,
|
||||
buyAds: (() -> Void)?
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
|
|
@ -44,7 +46,8 @@ final class StarsBalanceComponent: Component {
|
|||
self.actionAvailable = actionAvailable
|
||||
self.actionIsEnabled = actionIsEnabled
|
||||
self.actionCooldownUntilTimestamp = actionCooldownUntilTimestamp
|
||||
self.buy = buy
|
||||
self.action = action
|
||||
self.buyAds = buyAds
|
||||
}
|
||||
|
||||
static func ==(lhs: StarsBalanceComponent, rhs: StarsBalanceComponent) -> Bool {
|
||||
|
|
@ -83,6 +86,7 @@ final class StarsBalanceComponent: Component {
|
|||
private let title = ComponentView<Empty>()
|
||||
private let subtitle = ComponentView<Empty>()
|
||||
private var button = ComponentView<Empty>()
|
||||
private var buyAdsButton = ComponentView<Empty>()
|
||||
|
||||
private var component: StarsBalanceComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
|
@ -187,11 +191,18 @@ final class StarsBalanceComponent: Component {
|
|||
if component.actionAvailable {
|
||||
contentHeight += 12.0
|
||||
|
||||
var actionTitle = component.actionTitle
|
||||
var withdrawWidth = availableSize.width - sideInset * 2.0
|
||||
if let _ = component.buyAds {
|
||||
withdrawWidth = (withdrawWidth - 10.0) / 2.0
|
||||
actionTitle = component.strings.Stars_BotRevenue_Withdraw_WithdrawShort
|
||||
}
|
||||
|
||||
let content: AnyComponentWithIdentity<Empty>
|
||||
if remainingCooldownSeconds > 0 {
|
||||
content = AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(
|
||||
VStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(Text(text: component.actionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor))),
|
||||
AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(Text(text: actionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor))),
|
||||
AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(HStack([
|
||||
AnyComponentWithIdentity(id: 1, component: AnyComponent(BundleIconComponent(name: "Chat List/StatusLockIcon", tintColor: component.theme.list.itemCheckColors.fillColor.mixedWith(component.theme.list.itemCheckColors.foregroundColor, alpha: 0.7)))),
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(Text(text: stringForRemainingTime(remainingCooldownSeconds), font: Font.with(size: 11.0, weight: .medium, traits: [.monospacedNumbers]), color: component.theme.list.itemCheckColors.fillColor.mixedWith(component.theme.list.itemCheckColors.foregroundColor, alpha: 0.7))))
|
||||
|
|
@ -199,7 +210,7 @@ final class StarsBalanceComponent: Component {
|
|||
], spacing: 1.0)
|
||||
))
|
||||
} else {
|
||||
content = AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: component.actionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor)))
|
||||
content = AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: actionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor)))
|
||||
}
|
||||
|
||||
let buttonSize = self.button.update(
|
||||
|
|
@ -218,11 +229,11 @@ final class StarsBalanceComponent: Component {
|
|||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.buy()
|
||||
component.action()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
|
||||
containerSize: CGSize(width: withdrawWidth, height: 50.0)
|
||||
)
|
||||
if let buttonView = self.button.view {
|
||||
if buttonView.superview == nil {
|
||||
|
|
@ -231,6 +242,40 @@ final class StarsBalanceComponent: Component {
|
|||
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize)
|
||||
buttonView.frame = buttonFrame
|
||||
}
|
||||
|
||||
if let _ = component.buyAds {
|
||||
let buttonSize = self.buyAdsButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
color: component.theme.list.itemCheckColors.fillColor,
|
||||
foreground: component.theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
|
||||
),
|
||||
content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: component.strings.Stars_BotRevenue_Withdraw_BuyAds, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor))),
|
||||
isEnabled: component.actionIsEnabled,
|
||||
allowActionWhenDisabled: false,
|
||||
displaysProgress: false,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.buyAds?()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: withdrawWidth, height: 50.0)
|
||||
)
|
||||
if let buttonView = self.buyAdsButton.view {
|
||||
if buttonView.superview == nil {
|
||||
self.addSubview(buttonView)
|
||||
}
|
||||
let buttonFrame = CGRect(origin: CGPoint(x: sideInset + withdrawWidth + 10.0, y: contentHeight), size: buttonSize)
|
||||
buttonView.frame = buttonFrame
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
contentHeight += buttonSize.height
|
||||
}
|
||||
contentHeight += sideInset
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import UndoUI
|
|||
import ListItemComponentAdaptor
|
||||
import StatisticsUI
|
||||
import ItemListUI
|
||||
import StarsWithdrawalScreen
|
||||
|
||||
final class StarsStatisticsScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
|
@ -29,8 +30,9 @@ final class StarsStatisticsScreenComponent: Component {
|
|||
let revenueContext: StarsRevenueStatsContext
|
||||
let transactionsContext: StarsTransactionsContext
|
||||
let openTransaction: (StarsContext.State.Transaction) -> Void
|
||||
let buy: () -> Void
|
||||
let withdraw: () -> Void
|
||||
let showTimeoutTooltip: (Int32) -> Void
|
||||
let buyAds: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
|
|
@ -38,16 +40,18 @@ final class StarsStatisticsScreenComponent: Component {
|
|||
revenueContext: StarsRevenueStatsContext,
|
||||
transactionsContext: StarsTransactionsContext,
|
||||
openTransaction: @escaping (StarsContext.State.Transaction) -> Void,
|
||||
buy: @escaping () -> Void,
|
||||
showTimeoutTooltip: @escaping (Int32) -> Void
|
||||
withdraw: @escaping () -> Void,
|
||||
showTimeoutTooltip: @escaping (Int32) -> Void,
|
||||
buyAds: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.revenueContext = revenueContext
|
||||
self.transactionsContext = transactionsContext
|
||||
self.openTransaction = openTransaction
|
||||
self.buy = buy
|
||||
self.withdraw = withdraw
|
||||
self.showTimeoutTooltip = showTimeoutTooltip
|
||||
self.buyAds = buyAds
|
||||
}
|
||||
|
||||
static func ==(lhs: StarsStatisticsScreenComponent, rhs: StarsStatisticsScreenComponent) -> Bool {
|
||||
|
|
@ -67,22 +71,7 @@ final class StarsStatisticsScreenComponent: Component {
|
|||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override var contentOffset: CGPoint {
|
||||
set(value) {
|
||||
var value = value
|
||||
if value.y > self.contentSize.height - self.bounds.height {
|
||||
value.y = max(0.0, self.contentSize.height - self.bounds.height)
|
||||
self.bounces = false
|
||||
} else {
|
||||
self.bounces = true
|
||||
}
|
||||
super.contentOffset = value
|
||||
} get {
|
||||
return super.contentOffset
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
|
||||
return true
|
||||
|
|
@ -142,11 +131,7 @@ final class StarsStatisticsScreenComponent: Component {
|
|||
private var environment: Environment<ViewControllerComponentContainer.Environment>?
|
||||
private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)?
|
||||
private var controller: (() -> ViewController?)?
|
||||
|
||||
private var enableVelocityTracking: Bool = false
|
||||
private var previousVelocityM1: CGFloat = 0.0
|
||||
private var previousVelocity: CGFloat = 0.0
|
||||
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
|
||||
private var stateDisposable: Disposable?
|
||||
|
|
@ -208,20 +193,9 @@ final class StarsStatisticsScreenComponent: Component {
|
|||
deinit {
|
||||
self.stateDisposable?.dispose()
|
||||
}
|
||||
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
self.enableVelocityTracking = true
|
||||
}
|
||||
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
if self.enableVelocityTracking {
|
||||
self.previousVelocityM1 = self.previousVelocity
|
||||
if let value = (scrollView.value(forKey: (["_", "verticalVelocity"] as [String]).joined()) as? NSNumber)?.doubleValue {
|
||||
self.previousVelocity = CGFloat(value)
|
||||
}
|
||||
}
|
||||
|
||||
self.updateScrolling(transition: .immediate)
|
||||
|
||||
if let view = self.chartView.view as? ListItemComponentAdaptor.View, let node = view.itemNode as? StatsGraphItemNode {
|
||||
|
|
@ -229,31 +203,16 @@ final class StarsStatisticsScreenComponent: Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
guard let _ = self.navigationMetrics else {
|
||||
return
|
||||
}
|
||||
|
||||
let paneAreaExpansionDistance: CGFloat = 32.0
|
||||
let paneAreaExpansionFinalPoint: CGFloat = scrollView.contentSize.height - scrollView.bounds.height
|
||||
if targetContentOffset.pointee.y > paneAreaExpansionFinalPoint - paneAreaExpansionDistance && targetContentOffset.pointee.y < paneAreaExpansionFinalPoint {
|
||||
targetContentOffset.pointee.y = paneAreaExpansionFinalPoint
|
||||
self.enableVelocityTracking = false
|
||||
self.previousVelocity = 0.0
|
||||
self.previousVelocityM1 = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
private var lastScrollBounds: CGRect?
|
||||
private var lastBottomOffset: CGFloat?
|
||||
private func updateScrolling(transition: ComponentTransition) {
|
||||
guard let environment = self.environment?[ViewControllerComponentContainer.Environment.self].value else {
|
||||
return
|
||||
}
|
||||
|
||||
let scrollBounds = self.scrollView.bounds
|
||||
|
||||
let isLockedAtPanels = scrollBounds.maxY == self.scrollView.contentSize.height
|
||||
|
||||
|
||||
let topContentOffset = self.scrollView.contentOffset.y
|
||||
let navigationBackgroundAlpha = min(20.0, max(0.0, topContentOffset)) / 20.0
|
||||
|
||||
|
|
@ -267,6 +226,17 @@ final class StarsStatisticsScreenComponent: Component {
|
|||
|
||||
transition.setAlpha(layer: self.navigationSeparatorLayer, alpha: expansionDistanceFactor)
|
||||
|
||||
let bottomOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height)
|
||||
self.lastBottomOffset = bottomOffset
|
||||
|
||||
let transactionsScrollBounds: CGRect
|
||||
if let transactionsView = self.transactionsView.view {
|
||||
transactionsScrollBounds = CGRect(origin: CGPoint(x: 0.0, y: scrollBounds.origin.y - transactionsView.frame.minY), size: scrollBounds.size)
|
||||
} else {
|
||||
transactionsScrollBounds = .zero
|
||||
}
|
||||
self.lastScrollBounds = transactionsScrollBounds
|
||||
|
||||
let _ = self.transactionsView.updateEnvironment(
|
||||
transition: transition,
|
||||
environment: {
|
||||
|
|
@ -275,8 +245,10 @@ final class StarsStatisticsScreenComponent: Component {
|
|||
strings: environment.strings,
|
||||
dateTimeFormat: environment.dateTimeFormat,
|
||||
containerInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right),
|
||||
isScrollable: isLockedAtPanels,
|
||||
isCurrent: true
|
||||
isScrollable: false,
|
||||
isCurrent: true,
|
||||
externalScrollBounds: transactionsScrollBounds,
|
||||
externalBottomOffset: bottomOffset
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
@ -310,14 +282,7 @@ final class StarsStatisticsScreenComponent: Component {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
var wasLockedAtPanels = false
|
||||
if let panelContainerView = self.transactionsView.view, let navigationMetrics = self.navigationMetrics {
|
||||
if self.scrollView.bounds.minY > 0.0 && abs(self.scrollView.bounds.minY - (panelContainerView.frame.minY - navigationMetrics.navigationHeight)) <= UIScreenPixel {
|
||||
wasLockedAtPanels = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self.controller = environment.controller
|
||||
|
||||
self.navigationMetrics = (environment.navigationHeight, environment.statusBarHeight)
|
||||
|
|
@ -430,6 +395,13 @@ final class StarsStatisticsScreenComponent: Component {
|
|||
rate: starsState?.usdRate ?? 0.0
|
||||
))),
|
||||
AnyComponentWithIdentity(id: 1, component: AnyComponent(StarsOverviewItemComponent(
|
||||
theme: environment.theme,
|
||||
dateTimeFormat: environment.dateTimeFormat,
|
||||
title: strings.Stars_BotRevenue_Proceeds_Current,
|
||||
value: starsState?.balances.currentBalance ?? 0,
|
||||
rate: starsState?.usdRate ?? 0.0
|
||||
))),
|
||||
AnyComponentWithIdentity(id: 2, component: AnyComponent(StarsOverviewItemComponent(
|
||||
theme: environment.theme,
|
||||
dateTimeFormat: environment.dateTimeFormat,
|
||||
title: strings.Stars_BotRevenue_Proceeds_Total,
|
||||
|
|
@ -500,12 +472,12 @@ final class StarsStatisticsScreenComponent: Component {
|
|||
strings: strings,
|
||||
dateTimeFormat: environment.dateTimeFormat,
|
||||
count: self.starsState?.balances.availableBalance ?? 0,
|
||||
rate: 0.2,
|
||||
rate: self.starsState?.usdRate ?? 0,
|
||||
actionTitle: strings.Stars_BotRevenue_Withdraw_Withdraw,
|
||||
actionAvailable: true,
|
||||
actionIsEnabled: self.starsState?.balances.withdrawEnabled ?? true,
|
||||
actionCooldownUntilTimestamp: self.starsState?.balances.nextWithdrawalTimestamp,
|
||||
buy: { [weak self] in
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
|
@ -517,11 +489,17 @@ final class StarsStatisticsScreenComponent: Component {
|
|||
if remainingCooldownSeconds > 0 {
|
||||
component.showTimeoutTooltip(cooldownUntilTimestamp)
|
||||
} else {
|
||||
component.buy()
|
||||
component.withdraw()
|
||||
}
|
||||
} else {
|
||||
component.buy()
|
||||
component.withdraw()
|
||||
}
|
||||
},
|
||||
buyAds: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.buyAds()
|
||||
}
|
||||
)
|
||||
))]
|
||||
|
|
@ -585,8 +563,10 @@ final class StarsStatisticsScreenComponent: Component {
|
|||
strings: strings,
|
||||
dateTimeFormat: environment.dateTimeFormat,
|
||||
containerInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right),
|
||||
isScrollable: wasLockedAtPanels,
|
||||
isCurrent: true
|
||||
isScrollable: false,
|
||||
isCurrent: true,
|
||||
externalScrollBounds: self.lastScrollBounds ?? .zero,
|
||||
externalBottomOffset: self.lastBottomOffset ?? 1000
|
||||
)
|
||||
},
|
||||
containerSize: CGSize(width: availableSize.width - sideInsets, height: availableSize.height)
|
||||
|
|
@ -606,7 +586,6 @@ final class StarsStatisticsScreenComponent: Component {
|
|||
|
||||
self.ignoreScrolling = true
|
||||
|
||||
let contentOffset = self.scrollView.bounds.minY
|
||||
transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center)
|
||||
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
|
||||
if self.scrollView.contentSize != contentSize {
|
||||
|
|
@ -616,16 +595,8 @@ final class StarsStatisticsScreenComponent: Component {
|
|||
|
||||
var scrollViewBounds = self.scrollView.bounds
|
||||
scrollViewBounds.size = availableSize
|
||||
if wasLockedAtPanels, let panelContainerView = self.transactionsView.view {
|
||||
scrollViewBounds.origin.y = panelContainerView.frame.minY - environment.navigationHeight
|
||||
}
|
||||
transition.setBounds(view: self.scrollView, bounds: scrollViewBounds)
|
||||
|
||||
if !wasLockedAtPanels && !transition.animation.isImmediate && self.scrollView.bounds.minY != contentOffset {
|
||||
let deltaOffset = self.scrollView.bounds.minY - contentOffset
|
||||
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -deltaOffset), to: CGPoint(), additive: true)
|
||||
}
|
||||
|
||||
|
||||
self.ignoreScrolling = false
|
||||
|
||||
self.updateScrolling(transition: transition)
|
||||
|
|
@ -659,6 +630,7 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer {
|
|||
self.transactionsContext = context.engine.payments.peerStarsTransactionsContext(subject: .peer(peerId), mode: .all)
|
||||
|
||||
var withdrawImpl: (() -> Void)?
|
||||
var buyAdsImpl: (() -> Void)?
|
||||
var showTimeoutTooltipImpl: ((Int32) -> Void)?
|
||||
var openTransactionImpl: ((StarsContext.State.Transaction) -> Void)?
|
||||
super.init(context: context, component: StarsStatisticsScreenComponent(
|
||||
|
|
@ -669,11 +641,14 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer {
|
|||
openTransaction: { transaction in
|
||||
openTransactionImpl?(transaction)
|
||||
},
|
||||
buy: {
|
||||
withdraw: {
|
||||
withdrawImpl?()
|
||||
},
|
||||
showTimeoutTooltip: { timestamp in
|
||||
showTimeoutTooltipImpl?(timestamp)
|
||||
},
|
||||
buyAds: {
|
||||
buyAdsImpl?()
|
||||
}
|
||||
), navigationBarAppearance: .transparent)
|
||||
|
||||
|
|
@ -683,8 +658,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
|
||||
|
|
@ -705,7 +686,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
|
||||
}
|
||||
|
|
@ -787,6 +768,19 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
buyAdsImpl = {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let _ = (context.engine.peers.requestStarsRevenueAdsAccountlUrl(peerId: peerId)
|
||||
|> deliverOnMainQueue).startStandalone(next: { url in
|
||||
guard let url else {
|
||||
return
|
||||
}
|
||||
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
|
||||
})
|
||||
}
|
||||
|
||||
self.transactionsContext.loadMore()
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import TelegramStringFormatting
|
|||
import AvatarNode
|
||||
import BundleIconComponent
|
||||
import PhotoResources
|
||||
import StarsAvatarComponent
|
||||
|
||||
private extension StarsContext.State.Transaction {
|
||||
var extendedId: String {
|
||||
|
|
@ -168,8 +169,9 @@ final class StarsTransactionsListPanelComponent: Component {
|
|||
return
|
||||
}
|
||||
|
||||
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -100.0)
|
||||
|
||||
var visibleBounds = environment.externalScrollBounds ?? self.scrollView.bounds
|
||||
visibleBounds = visibleBounds.insetBy(dx: 0.0, dy: -100.0)
|
||||
|
||||
var validIds = Set<String>()
|
||||
if let visibleItems = itemLayout.visibleItems(for: visibleBounds) {
|
||||
for index in visibleItems.lowerBound ..< visibleItems.upperBound {
|
||||
|
|
@ -206,7 +208,10 @@ final class StarsTransactionsListPanelComponent: Component {
|
|||
var itemDate: String
|
||||
switch item.peer {
|
||||
case let .peer(peer):
|
||||
if let title = item.title {
|
||||
if !item.media.isEmpty {
|
||||
itemTitle = environment.strings.Stars_Intro_Transaction_MediaPurchase
|
||||
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 {
|
||||
|
|
@ -230,6 +235,9 @@ final class StarsTransactionsListPanelComponent: Component {
|
|||
case .premiumBot:
|
||||
itemTitle = environment.strings.Stars_Intro_Transaction_PremiumBotTopUp_Title
|
||||
itemSubtitle = environment.strings.Stars_Intro_Transaction_PremiumBotTopUp_Subtitle
|
||||
case .ads:
|
||||
itemTitle = environment.strings.Stars_Intro_Transaction_TelegramAds_Title
|
||||
itemSubtitle = environment.strings.Stars_Intro_Transaction_TelegramAds_Subtitle
|
||||
case .unsupported:
|
||||
itemTitle = environment.strings.Stars_Intro_Transaction_Unsupported_Title
|
||||
itemSubtitle = nil
|
||||
|
|
@ -290,9 +298,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, backgroundColor: environment.theme.list.plainBackgroundColor))), 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
|
||||
|
|
@ -342,7 +350,7 @@ final class StarsTransactionsListPanelComponent: Component {
|
|||
self.visibleItems.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
let bottomOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height)
|
||||
let bottomOffset = self.environment?.externalBottomOffset ?? max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height)
|
||||
let loadMore = bottomOffset < 100.0
|
||||
if environment.isCurrent, loadMore {
|
||||
let lastId = self.items.last?.extendedId
|
||||
|
|
@ -436,11 +444,18 @@ final class StarsTransactionsListPanelComponent: Component {
|
|||
|
||||
self.ignoreScrolling = true
|
||||
let contentOffset = self.scrollView.bounds.minY
|
||||
transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center)
|
||||
|
||||
var scrollBounds = self.scrollView.bounds
|
||||
scrollBounds.size = availableSize
|
||||
if !environment.isScrollable {
|
||||
if let _ = environment.externalScrollBounds {
|
||||
scrollBounds.origin = CGPoint()
|
||||
scrollBounds.size = CGSize(width: availableSize.width, height: itemLayout.contentHeight)
|
||||
transition.setPosition(view: self.scrollView, position: scrollBounds.center)
|
||||
} else {
|
||||
transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center)
|
||||
scrollBounds.size = availableSize
|
||||
if !environment.isScrollable {
|
||||
scrollBounds.origin = CGPoint()
|
||||
}
|
||||
}
|
||||
transition.setBounds(view: self.scrollView, bounds: scrollBounds)
|
||||
self.scrollView.isScrollEnabled = environment.isScrollable
|
||||
|
|
@ -456,10 +471,10 @@ final class StarsTransactionsListPanelComponent: Component {
|
|||
self.ignoreScrolling = false
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
if component.isAccount {
|
||||
return availableSize
|
||||
if let _ = environment.externalScrollBounds {
|
||||
return CGSize(width: availableSize.width, height: contentSize.height)
|
||||
} else {
|
||||
return CGSize(width: availableSize.width, height: min(availableSize.height, contentSize.height))
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -485,241 +500,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue