# Conflicts:
#	submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift
This commit is contained in:
Isaac 2024-06-24 18:20:30 +01:00
commit 779b583927
125 changed files with 5809 additions and 1836 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -54,6 +54,8 @@ public final class GalleryFooterNode: ASDisplayNode {
}
self.addSubnode(footerContentNode)
}
} else if let _ = self.currentThumbnailPanelHeight {
self.currentThumbnailPanelHeight = thumbnailPanelHeight
}
var animateOverlayIn = false

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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] = []

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -408,6 +408,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
attributes,
contentMediaValue,
nil,
nil,
automaticDownload ? .full : .none,
associatedData.automaticDownloadPeerType,
associatedData.automaticDownloadPeerId,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3295,7 +3295,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
default:
break
}
}, openCheckoutOrReceipt: { _ in
}, openCheckoutOrReceipt: { _, _ in
}, openSearch: {
}, setupReply: { _ in
}, canSetupReply: { _ in

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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