Paid media improvements

This commit is contained in:
Ilya Laktyushin 2024-06-22 00:05:21 +04:00
parent a545eac99e
commit b5b052ff7d
63 changed files with 3460 additions and 847 deletions

View file

@ -12290,12 +12290,14 @@ Sorry for the inconvenience.";
"Stars.Transaction.Via" = "Via";
"Stars.Transaction.To" = "To";
"Stars.Transaction.From" = "From";
"Stars.Transaction.Media" = "Media";
"Stars.Transaction.Id" = "Transaction ID";
"Stars.Transaction.Date" = "Date";
"Stars.Transaction.Terms" = "Review the [Terms of Service]() for Stars.";
"Stars.Transaction.Terms_URL" = "https://telegram.org/tos";
"Stars.Transaction.CopiedId" = "Transaction ID copied to clipboard.";
"Stars.Transaction.MediaPurchase" = "Media Purchase";
"Stars.Transaction.AppleTopUp.Title" = "Stars Top-Up";
"Stars.Transaction.AppleTopUp.Subtitle" = "App Store";
"Stars.Transaction.GoogleTopUp.Title" = "Stars Top-Up";
@ -12319,7 +12321,7 @@ Sorry for the inconvenience.";
"Stars.Transfer.Purchased.Stars_1" = "%@ Star";
"Stars.Transfer.Purchased.Stars_any" = "%@ Stars";
"Stars.Transfer.UnlockedText" = "You unlocked media for **%1$@**.";
"Stars.Transfer.UnlockInfo" = "Do you want to unlock media for **%1$@**?";
"Stars.Transfer.UnlockInfo" = "Do you want to unlock %1$@ in **%2$@** for **%3$@**?";
"Stars.Transfer.Balance" = "Balance";
@ -12349,16 +12351,16 @@ Sorry for the inconvenience.";
"HashtagSearch.StoriesFoundInfo" = "View stories with %@";
"Stars.BotRevenue.Title" = "Stars Balance";
"Stars.BotRevenue.Revenue.Title" = "Revenue";
"Stars.BotRevenue.Proceeds.Title" = "Proceeds Overview";
"Stars.BotRevenue.Revenue.Title" = "Stars Received";
"Stars.BotRevenue.Proceeds.Title" = "Rewards Overview";
"Stars.BotRevenue.Proceeds.Available" = "Available Balance";
"Stars.BotRevenue.Proceeds.Current" = "Total Balance";
"Stars.BotRevenue.Proceeds.Total" = "Total Lifetime Proceeds";
"Stars.BotRevenue.Proceeds.Info" = "Stars from your total balance become available for spending on ads and rewards 21 days after they are earned.";
"Stars.BotRevenue.Proceeds.Total" = "Lifetime Proceeds";
"Stars.BotRevenue.Proceeds.Info" = "Stars from your total balance can be used for ads or withdrawn as rewards 21 days after they are earned.";
"Stars.BotRevenue.Withdraw.Balance" = "Available Balance";
"Stars.BotRevenue.Withdraw.Withdraw" = "Withdraw via Fragment";
"Stars.BotRevenue.Withdraw.Info" = "You can withdraw Stars using Fragment, or use Stars to advertise your bot. [Learn More >]()";
"Stars.BotRevenue.Withdraw.Info" = "You can collect rewards for Stars using Fragment, or use Stars to advertise your bot. [Learn More >]()";
"Stars.BotRevenue.Withdraw.Info_URL" = "https://telegram.org/tos";
"Stars.BotRevenue.Transactions.Title" = "Transaction History";
@ -12403,3 +12405,18 @@ Sorry for the inconvenience.";
"Chat.MessagesDeletedToast.Text_1" = "Message Deleted";
"Chat.MessagesDeletedToast.Text_any" = "%d Messages Deleted";
"Monetization.StarsRevenueTitle" = "REVENUE";
"Monetization.TonBalanceTitle" = "TON BALANCE";
"Monetization.StarsBalanceTitle" = "STARS BALANCE";
"Monetization.TonTransactions" = "TON Transactions";
"Monetization.StarsTransactions" = "Stars Transactions";
"Monetization.BalanceStarsWithdraw" = "Withdraw via Fragment";
"Monetization.Balance.StarsInfo" = "You can withdraw Stars using Fragment, or use Stars to advertise your channel. [Learn More >]()";
"Monetization.Balance.StarsInfo_URL" = "https://telegram.org";
"Premium.MessageEffects" = "Message Effects";
"Premium.MessageEffectsInfo" = "Add over 500 animated effects to private messages.";

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

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

@ -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
@ -337,12 +406,6 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
if messageText.isEmpty, case let .Loaded(content) = webpage.content {
messageText = content.displayUrl
}
case _ as TelegramMediaPaidContent:
if message.text.isEmpty {
messageText = "Paid Media"
} else {
messageText = "🖼 \(messageText)"
}
default:
break
}

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,9 +111,9 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
}
}
for media in message.media {
if let paidContent = media as? TelegramMediaPaidContent, let extendedMedia = paidContent.extendedMedia.first, case let .full(fullMedia) = extendedMedia {
if let paidContent = media as? TelegramMediaPaidContent, let extendedMedia = paidContent.extendedMedia.first, case .full = extendedMedia {
standalone = true
galleryMedia = fullMedia
galleryMedia = paidContent
} else if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia {
standalone = true
galleryMedia = fullMedia
@ -241,7 +241,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
var source = source
if standalone {
source = .standaloneMessage(message)
source = .standaloneMessage(message, nil)
}
if internalDocumentItemSupportsMimeType(file.mimeType, fileName: file.fileName ?? "file") {
@ -280,7 +280,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
return .gallery(startState
|> deliverOnMainQueue
|> map { startState in
let gallery = GalleryController(context: context, source: source ?? (standalone ? .standaloneMessage(message) : .peerMessagesAtId(messageId: message.id, chatLocation: openChatLocation, customTag: chatFilterTag, chatLocationContextHolder: openChatLocationContextHolder)), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: startState.timecode, playbackRate: startState.rate, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in
let gallery = GalleryController(context: context, source: source ?? (standalone ? .standaloneMessage(message, mediaIndex) : .peerMessagesAtId(messageId: message.id, chatLocation: openChatLocation, customTag: chatFilterTag, chatLocationContextHolder: openChatLocationContextHolder)), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: startState.timecode, playbackRate: startState.rate, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in
navigationController?.replaceTopController(controller, animated: false, ready: ready)
}, baseNavigationController: navigationController, actionInteraction: actionInteraction)
gallery.temporaryDoNotWaitForReady = autoplayingVideo

View file

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

View file

@ -44,9 +44,7 @@ private func tagsForMessage(_ message: Message) -> MessageTags? {
}
private func galleryMediaForMedia(media: Media) -> Media? {
if let paidContent = media as? TelegramMediaPaidContent, let extendedMedia = paidContent.extendedMedia.first, case let .full(fullMedia) = extendedMedia {
return fullMedia
} else if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia {
if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia {
return fullMedia
} else if let media = media as? TelegramMediaImage {
return media
@ -62,22 +60,30 @@ private func galleryMediaForMedia(media: Media) -> Media? {
return nil
}
private func mediaForMessage(message: Message) -> (Media, TelegramMediaImage?)? {
private func mediaForMessage(message: Message) -> [(Media, TelegramMediaImage?)] {
for media in message.media {
if let result = galleryMediaForMedia(media: media) {
return (result, nil)
return [(result, nil)]
} else if let paidContent = media as? TelegramMediaPaidContent {
var results: [(Media, TelegramMediaImage?)] = []
for case let .full(fullMedia) in paidContent.extendedMedia {
if let result = galleryMediaForMedia(media: fullMedia) {
results.append((result, nil))
}
}
return results
} else if let webpage = media as? TelegramMediaWebpage {
switch webpage.content {
case let .Loaded(content):
if let embedUrl = content.embedUrl, !embedUrl.isEmpty {
return (webpage, nil)
return [(webpage, nil)]
} else if let file = content.file {
if let result = galleryMediaForMedia(media: file) {
return (result, content.image)
return [(result, content.image)]
}
} else if let image = content.image {
if let result = galleryMediaForMedia(media: image) {
return (result, nil)
return [(result, nil)]
}
}
case .Pending:
@ -85,7 +91,7 @@ private func mediaForMessage(message: Message) -> (Media, TelegramMediaImage?)?
}
}
}
return nil
return []
}
private let internalExtensions = Set<String>([
@ -176,7 +182,7 @@ private func galleryMessageCaptionText(_ message: Message) -> String {
public func galleryItemForEntry(
context: AccountContext,
presentationData: PresentationData,
entry: MessageHistoryEntry,
entry: GalleryEntry,
isCentral: Bool = false,
streamVideos: Bool,
loopVideos: Bool = false,
@ -198,195 +204,209 @@ public func galleryItemForEntry(
generateStoreAfterDownload: ((Message, TelegramMediaFile) -> (() -> Void)?)? = nil,
present: @escaping (ViewController, Any?) -> Void) -> GalleryItem?
{
let message = entry.message
let location = entry.location
if let (media, mediaImage) = mediaForMessage(message: message) {
if let _ = media as? TelegramMediaImage {
return ChatImageGalleryItem(
context: context,
presentationData: presentationData,
message: message,
location: location,
translateToLanguage: translateToLanguage,
peerIsCopyProtected: peerIsCopyProtected,
isSecret: isSecret,
displayInfoOnTop: displayInfoOnTop,
performAction: performAction,
openActionOptions: openActionOptions,
present: present
)
} else if let file = media as? TelegramMediaFile {
if file.isVideo {
let content: UniversalVideoContent
let captureProtected = message.isCopyProtected() || message.containsSecretMedia || message.minAutoremoveOrClearTimeout == viewOnceTimeout
if file.isAnimated {
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), loopVideo: true, enableSound: false, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
let message = entry.entry.message
let location = entry.location ?? entry.entry.location
let messageMedia = mediaForMessage(message: message)
let mediaAndMediaImage: (Media, TelegramMediaImage?)?
if let mediaIndex = entry.mediaIndex {
if mediaIndex < messageMedia.count {
mediaAndMediaImage = messageMedia[Int(mediaIndex)]
} else {
mediaAndMediaImage = nil
}
} else {
mediaAndMediaImage = messageMedia.first
}
guard let (media, mediaImage) = mediaAndMediaImage else {
return nil
}
if let _ = media as? TelegramMediaImage {
return ChatImageGalleryItem(
context: context,
presentationData: presentationData,
message: message,
mediaIndex: entry.mediaIndex,
location: location,
translateToLanguage: translateToLanguage,
peerIsCopyProtected: peerIsCopyProtected,
isSecret: isSecret,
displayInfoOnTop: displayInfoOnTop,
performAction: performAction,
openActionOptions: openActionOptions,
present: present
)
} else if let file = media as? TelegramMediaFile {
if file.isVideo {
let content: UniversalVideoContent
let captureProtected = message.isCopyProtected() || message.containsSecretMedia || message.minAutoremoveOrClearTimeout == viewOnceTimeout
if file.isAnimated {
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), loopVideo: true, enableSound: false, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
} else {
if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") {
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
} else {
if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") {
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
} else {
content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), content: .file(.message(message: MessageReference(message), media: file)), streamVideo: streamVideos, loopVideo: loopVideos)
}
content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), content: .file(.message(message: MessageReference(message), media: file)), streamVideo: streamVideos, loopVideo: loopVideos)
}
var entities: [MessageTextEntity] = []
}
var entities: [MessageTextEntity] = []
for attribute in message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
entities = attribute.entities
break
}
}
var text = galleryMessageCaptionText(message)
if let translateToLanguage, !text.isEmpty {
for attribute in message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage {
text = attribute.text
entities = attribute.entities
break
}
}
var text = galleryMessageCaptionText(message)
if let translateToLanguage, !text.isEmpty {
for attribute in message.attributes {
if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage {
text = attribute.text
entities = attribute.entities
break
}
}
}
if let result = addLocallyGeneratedEntities(text, enabledTypes: [.timecode], entities: entities, mediaDuration: file.duration.flatMap(Double.init)) {
entities = result
}
var originData = GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp)
if Namespaces.Message.allNonRegular.contains(message.id.namespace) {
originData = GalleryItemOriginData(title: nil, timestamp: nil)
}
let caption = galleryCaptionStringWithAppliedEntities(context: context, text: text, entities: entities, message: message)
return UniversalVideoGalleryItem(
context: context,
presentationData: presentationData,
content: content,
originData: originData,
indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) },
contentInfo: .message(message),
caption: caption,
displayInfoOnTop: displayInfoOnTop,
hideControls: hideControls,
fromPlayingVideo: fromPlayingVideo,
isSecret: isSecret,
landscape: landscape,
timecode: timecode,
peerIsCopyProtected: peerIsCopyProtected,
playbackRate: playbackRate,
configuration: configuration,
playbackCompleted: playbackCompleted,
performAction: performAction,
openActionOptions: openActionOptions,
storeMediaPlaybackState: storeMediaPlaybackState,
present: present
)
} else {
if let fileName = file.fileName, (fileName as NSString).pathExtension.lowercased() == "json" {
return ChatAnimationGalleryItem(context: context, presentationData: presentationData, message: message, location: location)
}
else if file.mimeType.hasPrefix("image/") && file.mimeType != "image/gif" {
var pixelsCount: Int = 0
if let dimensions = file.dimensions {
pixelsCount = Int(dimensions.width) * Int(dimensions.height)
}
if let result = addLocallyGeneratedEntities(text, enabledTypes: [.timecode], entities: entities, mediaDuration: file.duration.flatMap(Double.init)) {
entities = result
}
var originData = GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp)
if Namespaces.Message.allNonRegular.contains(message.id.namespace) {
originData = GalleryItemOriginData(title: nil, timestamp: nil)
}
let caption = galleryCaptionStringWithAppliedEntities(context: context, text: text, entities: entities, message: message)
return UniversalVideoGalleryItem(
context: context,
presentationData: presentationData,
content: content,
originData: originData,
indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) },
contentInfo: .message(message),
caption: caption,
displayInfoOnTop: displayInfoOnTop,
hideControls: hideControls,
fromPlayingVideo: fromPlayingVideo,
isSecret: isSecret,
landscape: landscape,
timecode: timecode,
peerIsCopyProtected: peerIsCopyProtected,
playbackRate: playbackRate,
configuration: configuration,
playbackCompleted: playbackCompleted,
performAction: performAction,
openActionOptions: openActionOptions,
storeMediaPlaybackState: storeMediaPlaybackState,
present: present
)
} else {
if let fileName = file.fileName, (fileName as NSString).pathExtension.lowercased() == "json" {
return ChatAnimationGalleryItem(context: context, presentationData: presentationData, message: message, location: location)
}
else if file.mimeType.hasPrefix("image/") && file.mimeType != "image/gif" {
var pixelsCount: Int = 0
if let dimensions = file.dimensions {
pixelsCount = Int(dimensions.width) * Int(dimensions.height)
}
if pixelsCount < 10000 * 10000 {
return ChatImageGalleryItem(
context: context,
presentationData: presentationData,
message: message,
location: location,
translateToLanguage: translateToLanguage,
peerIsCopyProtected: peerIsCopyProtected,
isSecret: isSecret,
displayInfoOnTop: displayInfoOnTop,
performAction: performAction,
openActionOptions: openActionOptions,
present: present
)
} else {
return ChatDocumentGalleryItem(
context: context,
presentationData: presentationData,
message: message,
location: location
)
}
} else if internalDocumentItemSupportsMimeType(file.mimeType, fileName: file.fileName) {
if pixelsCount < 10000 * 10000 {
return ChatImageGalleryItem(
context: context,
presentationData: presentationData,
message: message,
location: location,
translateToLanguage: translateToLanguage,
peerIsCopyProtected: peerIsCopyProtected,
isSecret: isSecret,
displayInfoOnTop: displayInfoOnTop,
performAction: performAction,
openActionOptions: openActionOptions,
present: present
)
} else {
return ChatDocumentGalleryItem(
context: context,
presentationData: presentationData,
message: message,
location: location
)
} else {
return ChatExternalFileGalleryItem(
context: context,
presentationData: presentationData,
message: message,
location: location
)
}
}
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(webpageContent) = webpage.content {
var content: UniversalVideoContent?
switch websiteType(of: webpageContent.websiteName) {
case .instagram where webpageContent.file != nil && webpageContent.image != nil && webpageContent.file!.isVideo:
content = NativeVideoContent(id: .message(message.stableId, webpageContent.file?.id ?? webpage.webpageId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: webpageContent.file!), imageReference: webpageContent.image.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, enableSound: true, captureProtected: message.isCopyProtected() || message.containsSecretMedia, storeAfterDownload: nil)
default:
if let embedUrl = webpageContent.embedUrl, let image = webpageContent.image {
if let file = webpageContent.file, file.isVideo {
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: message.isCopyProtected() || message.containsSecretMedia, storeAfterDownload: generateStoreAfterDownload?(message, file))
} else if URL(string: embedUrl)?.pathExtension == "mp4" {
content = SystemVideoContent(userLocation: .peer(message.id.peerId), url: embedUrl, imageReference: .webPage(webPage: WebpageReference(webpage), media: image), dimensions: webpageContent.embedSize?.cgSize ?? CGSize(width: 640.0, height: 640.0), duration: webpageContent.duration.flatMap(Double.init) ?? 0.0)
}
}
if content == nil, let webEmbedContent = WebEmbedVideoContent(userLocation: .peer(message.id.peerId), webPage: webpage, webpageContent: webpageContent, forcedTimestamp: timecode.flatMap(Int.init), openUrl: { url in
performAction(.url(url: url.absoluteString, concealed: false))
}) {
content = webEmbedContent
}
}
if let content = content {
var description: NSAttributedString?
if let descriptionText = webpageContent.text {
var entities: [MessageTextEntity] = []
if let result = addLocallyGeneratedEntities(descriptionText, enabledTypes: [.timecode], entities: entities, mediaDuration: 86400) {
entities = result
}
description = galleryCaptionStringWithAppliedEntities(context: context, text: descriptionText, entities: entities, message: message)
}
var originData = GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp)
if Namespaces.Message.allNonRegular.contains(message.id.namespace) {
originData = GalleryItemOriginData(title: nil, timestamp: nil)
}
return UniversalVideoGalleryItem(
} else if internalDocumentItemSupportsMimeType(file.mimeType, fileName: file.fileName) {
return ChatDocumentGalleryItem(
context: context,
presentationData: presentationData,
content: content,
originData: originData,
indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) },
contentInfo: .message(message),
caption: NSAttributedString(string: ""),
description: description,
displayInfoOnTop: displayInfoOnTop,
fromPlayingVideo: fromPlayingVideo,
isSecret: isSecret,
landscape: landscape,
timecode: timecode,
playbackRate: playbackRate,
configuration: configuration,
performAction: performAction,
openActionOptions: openActionOptions,
storeMediaPlaybackState: storeMediaPlaybackState,
present: present
message: message,
location: location
)
} else {
return nil
return ChatExternalFileGalleryItem(
context: context,
presentationData: presentationData,
message: message,
location: location
)
}
}
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(webpageContent) = webpage.content {
var content: UniversalVideoContent?
switch websiteType(of: webpageContent.websiteName) {
case .instagram where webpageContent.file != nil && webpageContent.image != nil && webpageContent.file!.isVideo:
content = NativeVideoContent(id: .message(message.stableId, webpageContent.file?.id ?? webpage.webpageId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: webpageContent.file!), imageReference: webpageContent.image.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, enableSound: true, captureProtected: message.isCopyProtected() || message.containsSecretMedia, storeAfterDownload: nil)
default:
if let embedUrl = webpageContent.embedUrl, let image = webpageContent.image {
if let file = webpageContent.file, file.isVideo {
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: message.isCopyProtected() || message.containsSecretMedia, storeAfterDownload: generateStoreAfterDownload?(message, file))
} else if URL(string: embedUrl)?.pathExtension == "mp4" {
content = SystemVideoContent(userLocation: .peer(message.id.peerId), url: embedUrl, imageReference: .webPage(webPage: WebpageReference(webpage), media: image), dimensions: webpageContent.embedSize?.cgSize ?? CGSize(width: 640.0, height: 640.0), duration: webpageContent.duration.flatMap(Double.init) ?? 0.0)
}
}
if content == nil, let webEmbedContent = WebEmbedVideoContent(userLocation: .peer(message.id.peerId), webPage: webpage, webpageContent: webpageContent, forcedTimestamp: timecode.flatMap(Int.init), openUrl: { url in
performAction(.url(url: url.absoluteString, concealed: false))
}) {
content = webEmbedContent
}
}
if let content = content {
var description: NSAttributedString?
if let descriptionText = webpageContent.text {
var entities: [MessageTextEntity] = []
if let result = addLocallyGeneratedEntities(descriptionText, enabledTypes: [.timecode], entities: entities, mediaDuration: 86400) {
entities = result
}
description = galleryCaptionStringWithAppliedEntities(context: context, text: descriptionText, entities: entities, message: message)
}
var originData = GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp)
if Namespaces.Message.allNonRegular.contains(message.id.namespace) {
originData = GalleryItemOriginData(title: nil, timestamp: nil)
}
return UniversalVideoGalleryItem(
context: context,
presentationData: presentationData,
content: content,
originData: originData,
indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) },
contentInfo: .message(message),
caption: NSAttributedString(string: ""),
description: description,
displayInfoOnTop: displayInfoOnTop,
fromPlayingVideo: fromPlayingVideo,
isSecret: isSecret,
landscape: landscape,
timecode: timecode,
playbackRate: playbackRate,
configuration: configuration,
performAction: performAction,
openActionOptions: openActionOptions,
storeMediaPlaybackState: storeMediaPlaybackState,
present: present
)
}
}
return nil
}
@ -493,6 +513,38 @@ public struct GalleryConfiguration {
}
}
public struct GalleryEntryStableId: Hashable {
public var stableId: UInt32
public var mediaIndex: Int?
}
public struct GalleryEntry {
public var entry: MessageHistoryEntry
public var mediaIndex: Int?
public var location: MessageHistoryEntryLocation?
public var stableId: GalleryEntryStableId {
return GalleryEntryStableId(stableId: self.entry.message.stableId, mediaIndex: self.mediaIndex)
}
}
private func galleryEntriesForMessageHistoryEntries(_ entries: [MessageHistoryEntry]) -> [GalleryEntry] {
var results: [GalleryEntry] = []
for entry in entries {
let mediaCount = mediaForMessage(message: entry.message).count
if mediaCount > 0 {
if mediaCount > 1 {
for i in 0 ..< mediaCount {
results.append(GalleryEntry(entry: entry, mediaIndex: i, location: MessageHistoryEntryLocation(index: i, count: mediaCount)))
}
} else {
results.append(GalleryEntry(entry: entry))
}
}
}
return results
}
public class GalleryController: ViewController, StandalonePresentableController, KeyShortcutResponder {
public static let darkNavigationTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), enableBackgroundBlur: false, separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
public static let lightNavigationTheme = NavigationBarTheme(buttonColor: UIColor(rgb: 0x007aff), disabledButtonColor: UIColor(rgb: 0xd0d0d0), primaryTextColor: .black, backgroundColor: UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0), enableBackgroundBlur: false, separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
@ -526,12 +578,12 @@ public class GalleryController: ViewController, StandalonePresentableController,
private let disposable = MetaDisposable()
private var peerIsCopyProtected = false
private var entries: [MessageHistoryEntry] = []
private var entries: [GalleryEntry] = []
private var hasLeftEntries: Bool = false
private var hasRightEntries: Bool = false
private var loadingMore: Bool = false
private var tag: HistoryViewInputTag?
private var centralEntryStableId: UInt32?
private var centralEntryStableId: GalleryEntryStableId?
private var configuration: GalleryConfiguration?
private let centralItemTitle = Promise<String>()
@ -650,7 +702,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
return nil
}
}
case let .standaloneMessage(m):
case let .standaloneMessage(m, _):
message = .single((m, m.isCopyProtected()))
case let .custom(messages, messageId, _):
message = messages
@ -732,24 +784,25 @@ public class GalleryController: ViewController, StandalonePresentableController,
let configuration = GalleryConfiguration.with(appConfiguration: appConfiguration)
strongSelf.configuration = configuration
let entries = view.entries
var centralEntryStableId: UInt32?
let entries = galleryEntriesForMessageHistoryEntries(view.entries)
var centralEntryStableId: GalleryEntryStableId?
loop: for i in 0 ..< entries.count {
let message = entries[i].message
let entry = entries[i]
let message = entry.entry.message
switch source {
case let .peerMessagesAtId(messageId, _, _, _):
if message.id == messageId {
centralEntryStableId = message.stableId
centralEntryStableId = entry.stableId
break loop
}
case let .standaloneMessage(m):
if message.id == m.id {
centralEntryStableId = message.stableId
case let .standaloneMessage(m, mediaIndex):
if message.id == m.id && entry.mediaIndex == mediaIndex {
centralEntryStableId = entry.stableId
break loop
}
case let .custom(_, messageId, _):
if message.id == messageId {
centralEntryStableId = message.stableId
centralEntryStableId = entry.stableId
break loop
}
}
@ -775,7 +828,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
var centralItemIndex: Int?
for entry in strongSelf.entries {
var isCentral = false
if entry.message.stableId == strongSelf.centralEntryStableId {
if entry.stableId == strongSelf.centralEntryStableId {
isCentral = true
}
if let item = galleryItemForEntry(context: context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: streamSingleVideo, fromPlayingVideo: isCentral && fromPlayingVideo, landscape: isCentral && landscape, timecode: isCentral ? timecode : nil, playbackRate: { return self?.playbackRate }, displayInfoOnTop: displayInfoOnTop, configuration: configuration, translateToLanguage: translateToLanguage, peerIsCopyProtected: view.peerIsCopyProtected, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, generateStoreAfterDownload: strongSelf.generateStoreAfterDownload, present: { [weak self] c, a in
@ -1227,13 +1280,25 @@ public class GalleryController: ViewController, StandalonePresentableController,
}
if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments {
let message = self.entries[centralItemNode.index].message
if let (media, _) = mediaForMessage(message: message), let transitionArguments = presentationArguments.transitionArguments(message.id, media), !forceAway {
animatedOutNode = false
centralItemNode.animateOut(to: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {
animatedOutNode = true
completion()
})
let entry = self.entries[centralItemNode.index]
let message = entry.entry.message
let media = mediaForMessage(message: message)
if !media.isEmpty {
var selectedMedia: Media?
if let mediaIndex = entry.mediaIndex {
if mediaIndex < media.count {
selectedMedia = media[Int(mediaIndex)].0
}
} else if let media = media.first {
selectedMedia = media.0
}
if let selectedMedia, let transitionArguments = presentationArguments.transitionArguments(message.id, selectedMedia), !forceAway {
animatedOutNode = false
centralItemNode.animateOut(to: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {
animatedOutNode = true
completion()
})
}
}
}
@ -1287,9 +1352,22 @@ public class GalleryController: ViewController, StandalonePresentableController,
self.galleryNode.transitionDataForCentralItem = { [weak self] in
if let strongSelf = self {
if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? GalleryControllerPresentationArguments {
let message = strongSelf.entries[centralItemNode.index].message
if let (media, _) = mediaForMessage(message: message), let transitionArguments = presentationArguments.transitionArguments(message.id, media) {
return (transitionArguments.transitionNode, transitionArguments.addToTransitionSurface)
let entry = strongSelf.entries[centralItemNode.index]
let message = entry.entry.message
let media = mediaForMessage(message: message)
if !media.isEmpty {
var selectedMedia: Media?
if let mediaIndex = entry.mediaIndex {
if mediaIndex < media.count {
selectedMedia = media[Int(mediaIndex)].0
}
} else if let media = media.first {
selectedMedia = media.0
}
if let selectedMedia, let transitionArguments = presentationArguments.transitionArguments(message.id, selectedMedia) {
return (transitionArguments.transitionNode, transitionArguments.addToTransitionSurface)
}
}
}
}
@ -1355,7 +1433,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
var centralItemIndex: Int?
for entry in self.entries {
var isCentral = false
if entry.message.stableId == self.centralEntryStableId {
if entry.stableId == self.centralEntryStableId {
isCentral = true
}
if let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: entry, streamVideos: self.streamVideos, fromPlayingVideo: isCentral && self.fromPlayingVideo, landscape: isCentral && self.landscape, timecode: isCentral ? self.timecode : nil, playbackRate: { [weak self] in return self?.playbackRate }, displayInfoOnTop: displayInfoOnTop, configuration: self.configuration, peerIsCopyProtected: self.peerIsCopyProtected, performAction: self.performAction, openActionOptions: self.openActionOptions, storeMediaPlaybackState: self.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, generateStoreAfterDownload: self.generateStoreAfterDownload, present: { [weak self] c, a in
@ -1376,11 +1454,19 @@ public class GalleryController: ViewController, StandalonePresentableController,
if let strongSelf = self {
var hiddenItem: (MessageId, Media)?
if let index = index {
let message = strongSelf.entries[index].message
let entry = strongSelf.entries[index]
let message = strongSelf.entries[index].entry.message
strongSelf.centralEntryStableId = message.stableId
if let (media, _) = mediaForMessage(message: message) {
hiddenItem = (message.id, media)
strongSelf.centralEntryStableId = entry.stableId
let media = mediaForMessage(message: message)
if !media.isEmpty {
if let mediaIndex = entry.mediaIndex {
if mediaIndex < media.count {
hiddenItem = (message.id, media[Int(mediaIndex)].0)
}
} else if let media = media.first {
hiddenItem = (message.id, media.0)
}
}
if let node = strongSelf.galleryNode.pager.centralItemNode() {
@ -1397,9 +1483,9 @@ public class GalleryController: ViewController, StandalonePresentableController,
case let .peerMessagesAtId(_, chatLocation, _, chatLocationContextHolder):
var reloadAroundIndex: MessageIndex?
if index <= 2 && strongSelf.hasLeftEntries {
reloadAroundIndex = strongSelf.entries.first?.index
reloadAroundIndex = strongSelf.entries.first?.entry.index
} else if index >= strongSelf.entries.count - 3 && strongSelf.hasRightEntries {
reloadAroundIndex = strongSelf.entries.last?.index
reloadAroundIndex = strongSelf.entries.last?.entry.index
}
let peerIsCopyProtected = strongSelf.peerIsCopyProtected
if let reloadAroundIndex = reloadAroundIndex, let tag = strongSelf.tag {
@ -1424,8 +1510,8 @@ public class GalleryController: ViewController, StandalonePresentableController,
return
}
let entries = view.entries
let entries = galleryEntriesForMessageHistoryEntries(view.entries)
if strongSelf.invertItemOrder {
strongSelf.entries = entries.reversed()
strongSelf.hasLeftEntries = view.hasLater
@ -1440,7 +1526,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
var centralItemIndex: Int?
for entry in strongSelf.entries {
var isCentral = false
if entry.message.stableId == strongSelf.centralEntryStableId {
if entry.stableId == strongSelf.centralEntryStableId {
isCentral = true
}
if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, playbackRate: { return self?.playbackRate }, displayInfoOnTop: displayInfoOnTop, configuration: strongSelf.configuration, peerIsCopyProtected: view.peerIsCopyProtected, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, generateStoreAfterDownload: strongSelf.generateStoreAfterDownload, present: { [weak self] c, a in
@ -1470,12 +1556,13 @@ public class GalleryController: ViewController, StandalonePresentableController,
return
}
var entries: [MessageHistoryEntry] = []
var messageEntries: [MessageHistoryEntry] = []
var index = messages.count
for message in messages.reversed() {
entries.append(MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)))
messageEntries.append(MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)))
index -= 1
}
let entries = galleryEntriesForMessageHistoryEntries(messageEntries)
if entries.count > strongSelf.entries.count {
if strongSelf.invertItemOrder {
@ -1492,7 +1579,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
var centralItemIndex: Int?
for entry in strongSelf.entries {
var isCentral = false
if entry.message.stableId == strongSelf.centralEntryStableId {
if entry.stableId == strongSelf.centralEntryStableId {
isCentral = true
}
if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, playbackRate: { return self?.playbackRate }, displayInfoOnTop: displayInfoOnTop, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, generateStoreAfterDownload: strongSelf.generateStoreAfterDownload, present: { [weak self] c, a in
@ -1551,7 +1638,8 @@ public class GalleryController: ViewController, StandalonePresentableController,
var nodeAnimatesItself = false
if let centralItemNode = self.galleryNode.pager.centralItemNode() {
let message = self.entries[centralItemNode.index].message
let entry = self.entries[centralItemNode.index]
self.centralItemTitle.set(centralItemNode.title())
self.centralItemTitleView.set(centralItemNode.titleView())
self.centralItemRightBarButtonItem.set(centralItemNode.rightBarButtonItem())
@ -1559,17 +1647,30 @@ public class GalleryController: ViewController, StandalonePresentableController,
self.centralItemNavigationStyle.set(centralItemNode.navigationStyle())
self.centralItemFooterContentNode.set(centralItemNode.footerContent())
self.galleryNode.pager.pagingEnabledPromise.set(centralItemNode.isPagingEnabled())
if let (media, _) = mediaForMessage(message: message) {
if let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments, let transitionArguments = presentationArguments.transitionArguments(message.id, media) {
nodeAnimatesItself = true
if presentationArguments.animated {
centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {})
let message = entry.entry.message
let media = mediaForMessage(message: message)
if !media.isEmpty {
var selectedMedia: Media?
if let mediaIndex = entry.mediaIndex {
if mediaIndex < media.count {
selectedMedia = media[Int(mediaIndex)].0
}
self._hiddenMedia.set(.single((message.id, media)))
} else if let media = media.first {
selectedMedia = media.0
}
if let selectedMedia {
if let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments, let transitionArguments = presentationArguments.transitionArguments(message.id, selectedMedia) {
nodeAnimatesItself = true
if presentationArguments.animated {
centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {})
}
self._hiddenMedia.set(.single((message.id, selectedMedia)))
}
centralItemNode.activateAsInitial()
}
centralItemNode.activateAsInitial()
}
self.onDidAppear?()
@ -1591,7 +1692,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
override public func didAppearInContextPreview() {
if let centralItemNode = self.galleryNode.pager.centralItemNode() {
let message = self.entries[centralItemNode.index].message
let message = self.entries[centralItemNode.index].entry.message
self.centralItemTitle.set(centralItemNode.title())
self.centralItemTitleView.set(centralItemNode.titleView())
self.centralItemRightBarButtonItem.set(centralItemNode.rightBarButtonItem())
@ -1600,7 +1701,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
self.centralItemFooterContentNode.set(centralItemNode.footerContent())
self.galleryNode.pager.pagingEnabledPromise.set(centralItemNode.isPagingEnabled())
if let _ = mediaForMessage(message: message) {
if !mediaForMessage(message: message).isEmpty {
centralItemNode.activateAsInitial()
}
}

View file

@ -112,6 +112,7 @@ class ChatImageGalleryItem: GalleryItem {
let context: AccountContext
let presentationData: PresentationData
let message: Message
let mediaIndex: Int?
let location: MessageHistoryEntryLocation?
let translateToLanguage: String?
let peerIsCopyProtected: Bool
@ -121,10 +122,11 @@ class ChatImageGalleryItem: GalleryItem {
let openActionOptions: (GalleryControllerInteractionTapAction, Message) -> Void
let present: (ViewController, Any?) -> Void
init(context: AccountContext, presentationData: PresentationData, message: Message, location: MessageHistoryEntryLocation?, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false, isSecret: Bool = false, displayInfoOnTop: Bool, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
init(context: AccountContext, presentationData: PresentationData, message: Message, mediaIndex: Int? = nil, location: MessageHistoryEntryLocation?, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false, isSecret: Bool = false, displayInfoOnTop: Bool, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
self.context = context
self.presentationData = presentationData
self.message = message
self.mediaIndex = mediaIndex
self.location = location
self.translateToLanguage = translateToLanguage
self.peerIsCopyProtected = peerIsCopyProtected
@ -140,8 +142,11 @@ class ChatImageGalleryItem: GalleryItem {
node.setMessage(self.message, displayInfo: !self.displayInfoOnTop, translateToLanguage: self.translateToLanguage, peerIsCopyProtected: self.peerIsCopyProtected, isSecret: self.isSecret)
for media in self.message.media {
if let paidContent = media as? TelegramMediaPaidContent, let extendedMedia = paidContent.extendedMedia.first, case let .full(fullMedia) = extendedMedia, let image = fullMedia as? TelegramMediaImage {
node.setImage(userLocation: .peer(self.message.id.peerId), imageReference: .message(message: MessageReference(self.message), media: image))
if let paidContent = media as? TelegramMediaPaidContent {
let mediaIndex = self.mediaIndex ?? 0
if case let .full(fullMedia) = paidContent.extendedMedia[Int(mediaIndex)], let image = fullMedia as? TelegramMediaImage {
node.setImage(userLocation: .peer(self.message.id.peerId), imageReference: .message(message: MessageReference(self.message), media: image))
}
} else if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia, let image = fullMedia as? TelegramMediaImage {
node.setImage(userLocation: .peer(self.message.id.peerId), imageReference: .message(message: MessageReference(self.message), media: image))
} else if let image = media as? TelegramMediaImage {
@ -528,7 +533,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
})
})))
if !message.isCopyProtected() && !self.peerIsCopyProtected, let media = self.contextAndMedia?.1 {
if !message.isCopyProtected() && !self.peerIsCopyProtected && message.paidContent == nil, let media = self.contextAndMedia?.1 {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Gallery_SaveImage, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in
f(.default)

View file

@ -2573,7 +2573,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
}
if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected {
if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveVideo, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in
f(.default)

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

@ -131,12 +131,14 @@ final class MediaPickerGridItemNode: GridItemNode {
private var interaction: MediaPickerInteraction?
private var theme: PresentationTheme?
private struct SelectionState: Equatable {
let selected: Bool
let count: Int
}
private let selectionPromise = ValuePromise<SelectionState>(SelectionState(selected: false, count: 0))
private let spoilerDisposable = MetaDisposable()
var spoilerNode: SpoilerOverlayNode?
var priceBackgroundNode: NavigationBackgroundNode?
var priceIconNode: ASImageNode?
var priceLabelNode: ImmediateTextNode?
var priceNode: PriceNode?
private let progressDisposable = MetaDisposable()
@ -252,7 +254,7 @@ final class MediaPickerGridItemNode: GridItemNode {
self.setNeedsLayout()
}
if let interaction = self.interaction, let selectionState = interaction.selectionState {
if let interaction = self.interaction, let selectionState = interaction.selectionState {
let selected = selectionState.isIdentifierSelected(self.identifier)
if let selectableItem = self.selectableItem {
let index = selectionState.index(of: selectableItem)
@ -261,6 +263,7 @@ final class MediaPickerGridItemNode: GridItemNode {
}
}
self.checkNode?.setSelected(selected, animated: animated)
self.selectionPromise.set(SelectionState(selected: selected, count: selectionState.selectedItems().count))
}
}
@ -288,6 +291,7 @@ final class MediaPickerGridItemNode: GridItemNode {
self.typeIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.durationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.draftNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.priceNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
if animateSpoilerNode {
self.spoilerNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
@ -564,12 +568,12 @@ final class MediaPickerGridItemNode: GridItemNode {
}
}
self.spoilerDisposable.set((combineLatest(spoilerSignal, priceSignal)
|> deliverOnMainQueue).start(next: { [weak self] hasSpoiler, price in
self.spoilerDisposable.set((combineLatest(spoilerSignal, priceSignal, self.selectionPromise.get())
|> deliverOnMainQueue).start(next: { [weak self] hasSpoiler, price, selectionState in
guard let strongSelf = self else {
return
}
strongSelf.updateHasSpoiler(hasSpoiler, price: price)
strongSelf.updateHasSpoiler(hasSpoiler, price: selectionState.selected ? price : nil, isSingle: selectionState.count == 1)
}))
if self.currentDraftState != nil {
@ -635,7 +639,7 @@ final class MediaPickerGridItemNode: GridItemNode {
}
private var didSetupSpoiler = false
private func updateHasSpoiler(_ hasSpoiler: Bool, price: Int64?) {
private func updateHasSpoiler(_ hasSpoiler: Bool, price: Int64?, isSingle: Bool) {
var animated = true
if !self.didSetupSpoiler {
animated = false
@ -659,49 +663,35 @@ final class MediaPickerGridItemNode: GridItemNode {
self.spoilerNode?.frame = CGRect(origin: .zero, size: bounds.size)
if let price {
let backgroundNode: NavigationBackgroundNode
let labelNode: ImmediateTextNode
let iconNode: ASImageNode
if let currentBackground = self.priceBackgroundNode, let currentLabel = self.priceLabelNode, let currentIcon = self.priceIconNode {
backgroundNode = currentBackground
labelNode = currentLabel
iconNode = currentIcon
let priceNode: PriceNode
if let currentPriceNode = self.priceNode {
priceNode = currentPriceNode
} else {
backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x000000, alpha: 0.5), enableBlur: true)
labelNode = ImmediateTextNode()
iconNode = ASImageNode()
iconNode.displaysAsynchronously = false
iconNode.image = UIImage(bundleImageName: "Premium/Stars/StarSmall")
priceNode = PriceNode()
if let spoilerNode = self.spoilerNode {
self.insertSubnode(backgroundNode, aboveSubnode: spoilerNode)
self.insertSubnode(priceNode, aboveSubnode: spoilerNode)
}
backgroundNode.addSubnode(labelNode)
backgroundNode.addSubnode(iconNode)
self.priceNode = priceNode
self.priceBackgroundNode = backgroundNode
self.priceLabelNode = labelNode
self.priceIconNode = iconNode
if animated {
priceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
labelNode.attributedText = NSAttributedString(string: "\(price)", font: Font.semibold(15.0), textColor: .white)
let labelSize = labelNode.updateLayout(CGSize(width: 200.0, height: 50.0))
let size = CGSize(width: labelSize.width + 40.0, height: 34.0)
backgroundNode.update(size: size, cornerRadius: 17.0, transition: .immediate)
backgroundNode.frame = CGRect(origin: CGPoint(x: floor((bounds.width - size.width) / 2.0), y: floor((bounds.height - size.height) / 2.0)), size: size)
if let icon = iconNode.image {
iconNode.frame = CGRect(origin: CGPoint(x: 10.0, y: floor((size.height - icon.size.height) / 2.0)), size: icon.size)
}
labelNode.frame = CGRect(origin: CGPoint(x: 30.0, y: floor((size.height - labelSize.height) / 2.0)), size: labelSize)
self.priceNode?.update(size: bounds.size, price: isSingle ? price : nil, small: true, transition: .immediate)
}
} else if let spoilerNode = self.spoilerNode {
self.spoilerNode = nil
spoilerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak spoilerNode] _ in
spoilerNode?.removeFromSupernode()
})
if let priceNode = self.priceNode {
self.priceNode = nil
priceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak priceNode] _ in
priceNode?.removeFromSupernode()
})
}
}
}

View file

@ -2496,30 +2496,6 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self?.groupedValue = false
self?.controllerNode.send(asFile: false, silently: false, scheduleTime: nil, animated: true, parameters: nil, completion: {})
})))
// if !items.isEmpty {
// items.append(.separator)
// }
// items.append(.action(ContextMenuActionItem(text: strings.Attachment_Grouped, icon: { theme in
// if !grouped {
// return nil
// }
// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
// }, action: { [weak self] _, f in
// f(.default)
//
// self?.groupedValue = true
// })))
// items.append(.action(ContextMenuActionItem(text: strings.Attachment_Ungrouped, icon: { theme in
// if grouped {
// return nil
// }
// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
// }, action: { [weak self] _, f in
// f(.default)
//
// self?.groupedValue = false
// })))
}
var isPaidAvailable = false
@ -2585,7 +2561,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
return
}
let controller = self.context.sharedContext.makeStarsAmountScreen(context: self.context, completion: { [weak self] amount in
let controller = self.context.sharedContext.makeStarsAmountScreen(context: self.context, initialValue: price, completion: { [weak self] amount in
guard let strongSelf = self else {
return
}

View file

@ -478,6 +478,81 @@ private class MediaPickerSelectedItemNode: ASDisplayNode {
}
}
final class PriceNode: ASDisplayNode {
let backgroundNode: NavigationBackgroundNode
let iconNode: ASImageNode
let lockNode: ASImageNode
let labelNode: ImmediateTextNode
override init() {
self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x000000, alpha: 0.35), enableBlur: true)
self.lockNode = ASImageNode()
self.lockNode.displaysAsynchronously = false
self.lockNode.image = generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Lock"), color: .white)
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.image = UIImage(bundleImageName: "Premium/Stars/StarSmall")
self.labelNode = ImmediateTextNode()
super.init()
self.addSubnode(self.backgroundNode)
self.backgroundNode.addSubnode(self.lockNode)
self.backgroundNode.addSubnode(self.iconNode)
self.backgroundNode.addSubnode(self.labelNode)
}
func update(size: CGSize, price: Int64?, small: Bool, transition: ContainedViewLayoutTransition) {
var nodeSize = CGSize(width: 50.0, height: 34.0)
var labelSize: CGSize = .zero
var backgroundTransition = transition
let labelTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
if let price {
//TODO:localize
self.labelNode.attributedText = NSAttributedString(string: "\(price)", font: Font.semibold(15.0), textColor: .white)
labelSize = self.labelNode.updateLayout(CGSize(width: 240.0, height: 50.0))
nodeSize.width = labelSize.width + 40.0
if self.labelNode.alpha != 1.0 && self.backgroundNode.frame.width > 0.0 {
backgroundTransition = labelTransition
}
labelTransition.updateAlpha(node: self.labelNode, alpha: 1.0)
labelTransition.updateAlpha(node: self.lockNode, alpha: 0.0)
} else {
if self.labelNode.alpha != 0.0 && self.backgroundNode.frame.width > 0.0 {
backgroundTransition = labelTransition
}
labelTransition.updateAlpha(node: self.labelNode, alpha: 0.0)
labelTransition.updateAlpha(node: self.lockNode, alpha: 1.0)
}
self.backgroundNode.update(size: nodeSize, cornerRadius: 17.0, transition: backgroundTransition)
backgroundTransition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: floor((size.width - nodeSize.width) / 2.0), y: floor((size.height - nodeSize.height) / 2.0)), size: nodeSize))
if let _ = price {
if let icon = self.iconNode.image {
self.iconNode.frame = CGRect(origin: CGPoint(x: 9.0 - UIScreenPixel, y: floor((nodeSize.height - icon.size.height) / 2.0)), size: icon.size)
}
self.labelNode.frame = CGRect(origin: CGPoint(x: 30.0, y: floor((nodeSize.height - labelSize.height) / 2.0)), size: labelSize)
} else {
if let icon = self.iconNode.image {
self.iconNode.frame = CGRect(origin: CGPoint(x: 9.0 - UIScreenPixel, y: floor((nodeSize.height - icon.size.height) / 2.0)), size: icon.size)
}
if let icon = self.lockNode.image {
self.lockNode.frame = CGRect(origin: CGPoint(x: 28.0, y: floor((nodeSize.height - icon.size.height) / 2.0)), size: icon.size)
}
}
}
}
private class MessageBackgroundNode: ASDisplayNode {
private let backgroundWallpaperNode: ChatMessageBubbleBackdrop
private let backgroundNode: ChatMessageBackground
@ -535,6 +610,7 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS
private let scrollNode: ASScrollNode
private var backgroundNodes: [Int: MessageBackgroundNode] = [:]
private var itemNodes: [String: MediaPickerSelectedItemNode] = [:]
private var priceNodes: [Int: PriceNode] = [:]
private var reorderFeedback: HapticFeedback?
private var reorderNode: ReorderingItemNode?
@ -649,6 +725,14 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS
}
}
for (_, priceNode) in strongSelf.priceNodes {
priceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.1)
if strongSelf.isExternalPreview {
ComponentTransition.immediate.setScale(layer: priceNode.layer, scale: 0.001)
transition.updateTransformScale(layer: priceNode.layer, scale: 1.0)
}
}
for (identifier, itemNode) in strongSelf.itemNodes {
if !strongSelf.isObscuredExternalPreview, let (transitionView, _, _) = strongSelf.getTransitionView(identifier) {
itemNode.animateFrom(transitionView, transition: transition)
@ -690,6 +774,10 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS
itemNode.layer.removeAllAnimations()
}
for (_, priceNode) in strongSelf.priceNodes {
priceNode.layer.removeAllAnimations()
}
strongSelf.messageNodes?.first?.layer.removeAllAnimations()
strongSelf.messageNodes?.last?.layer.removeAllAnimations()
@ -710,6 +798,13 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS
}
}
for (_, priceNode) in self.priceNodes {
priceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false)
if self.isExternalPreview {
transition.updateTransformScale(layer: priceNode.layer, scale: 0.001)
}
}
for (identifier, itemNode) in self.itemNodes {
if !self.isObscuredExternalPreview, let (transitionView, maybeDustNode, completion) = self.getTransitionView(identifier) {
itemNode.animateTo(transitionView, dustNode: maybeDustNode, transition: transition, completion: completion)
@ -844,6 +939,8 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS
let sideInset: CGFloat = 34.0
let boundingWidth = min(320.0, size.width - insets.left - insets.right - sideInset * 2.0)
var price: Int64?
var validIds: [String] = []
for item in items {
guard let asset = item as? TGMediaEditableItem, let identifier = asset.uniqueIdentifier else {
@ -872,6 +969,10 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS
} else {
itemSizes.append(asset.originalSize ?? CGSize())
}
if price == nil, let priceValue = self.interaction?.editingState.price(for: asset) as? Int64 {
price = priceValue
}
}
if !self.didSetReady {
@ -1062,6 +1163,31 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS
}
}
if let price {
let priceNode: PriceNode
if let current = self.priceNodes[groupIndex] {
priceNode = current
} else {
priceNode = PriceNode()
self.priceNodes[groupIndex] = priceNode
self.scrollNode.addSubnode(priceNode)
}
if priceNode.frame.width.isZero {
itemTransition = .immediate
}
let priceNodeFrame = groupRect
itemTransition.updatePosition(node: priceNode, position: priceNodeFrame.center)
itemTransition.updateBounds(node: priceNode, bounds: CGRect(origin: CGPoint(), size: priceNodeFrame.size))
priceNode.update(size: priceNode.frame.size, price: price, small: false, transition: itemTransition)
} else if let priceNode = self.priceNodes[groupIndex] {
self.priceNodes[groupIndex] = nil
priceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak priceNode] _ in
priceNode?.removeFromSupernode()
})
}
contentHeight += groupSize.height
contentWidth = max(contentWidth, groupSize.width)
groupIndex += 1
@ -1069,7 +1195,7 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS
if let dragNode = self.messageNodes?.last {
transition.updateAlpha(node: dragNode, alpha: items.count > 1 ? 1.0 : 0.0)
transition.updateFrame(node: dragNode, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top + contentHeight + 1.0), size: dragNode.frame.size))
transition.updateFrame(node: dragNode, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top + contentHeight + 9.0), size: dragNode.frame.size))
var dragNodeFrame = dragNode.frame
dragNodeFrame.origin.y = size.height - dragNodeFrame.origin.y - dragNodeFrame.size.height

View file

@ -1077,6 +1077,26 @@ private final class DemoSheetContent: CombinedComponent {
)
)
)
//TODO:localize
availableItems[.messageEffects] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.messageEffects,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .top,
model: .island,
videoFile: configuration.videos["effects"],
decoration: .swirlStars
)),
title: strings.Premium_MessageEffects,
text: strings.Premium_MessageEffectsInfo,
textColor: textColor
)
)
)
)
let index: Int = 0
var items: [DemoPagerComponent.Item] = []
@ -1172,6 +1192,9 @@ private final class DemoSheetContent: CombinedComponent {
text = strings.Premium_MessagePrivacyInfo
case .folderTags:
text = strings.Premium_FolderTagsStandaloneInfo
case .messageEffects:
//TODO:localize
text = "Add over 500 animated effects to private messages."
default:
text = ""
}
@ -1441,6 +1464,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
case messagePrivacy
case business
case folderTags
case messageEffects
case businessLocation
case businessHours
@ -1497,6 +1521,8 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
return .business
case .folderTags:
return .folderTags
case .messageEffects:
return .messageEffects
case .businessLocation:
return .businessLocation
case .businessHours:

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,26 @@ public class PremiumLimitsListScreen: ViewController {
)
)
)
//TODO:localize
availableItems[.messageEffects] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.messageEffects,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
model: .island,
videoFile: videos["effects"],
decoration: .swirlStars
)),
title: strings.Premium_MessageEffects,
text: strings.Premium_MessageEffectsInfo,
textColor: textColor
)
)
)
)
availableItems[.business] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.business,

View file

@ -50,6 +50,10 @@ swift_library(
"//submodules/PasswordSetupUI",
"//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController",
"//submodules/TelegramUI/Components/ListItemComponentAdaptor",
"//submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/Stars/StarsAvatarComponent",
],
visibility = [
"//visibility:public",

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,20 @@ private final class ChannelStatsControllerArguments {
let openGifts: () -> Void
let createPrepaidGiveaway: (PrepaidGiveaway) -> Void
let updateGiftsSelected: (Bool) -> Void
let updateStarsSelected: (Bool) -> Void
let requestWithdraw: () -> Void
let requestTonWithdraw: () -> Void
let requestStarsWithdraw: () -> Void
let openMonetizationIntro: () -> Void
let openMonetizationInfo: () -> Void
let openTransaction: (RevenueStatsTransactionsContext.State.Transaction) -> Void
let openTonTransaction: (RevenueStatsTransactionsContext.State.Transaction) -> Void
let openStarsTransaction: (StarsContext.State.Transaction) -> Void
let expandTransactions: () -> Void
let updateCpmEnabled: (Bool) -> Void
let presentCpmLocked: () -> Void
let dismissInput: () -> Void
init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openPostStats: @escaping (EnginePeer, StatsPostItem) -> Void, openStory: @escaping (EngineStoryItem, UIView) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openBoost: @escaping (ChannelBoostersContext.State.Boost) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void, requestWithdraw: @escaping () -> Void, openMonetizationIntro: @escaping () -> Void, openMonetizationInfo: @escaping () -> Void, openTransaction: @escaping (RevenueStatsTransactionsContext.State.Transaction) -> Void, expandTransactions: @escaping () -> Void, updateCpmEnabled: @escaping (Bool) -> Void, presentCpmLocked: @escaping () -> Void, dismissInput: @escaping () -> Void) {
init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openPostStats: @escaping (EnginePeer, StatsPostItem) -> Void, openStory: @escaping (EngineStoryItem, UIView) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openBoost: @escaping (ChannelBoostersContext.State.Boost) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void, updateStarsSelected: @escaping (Bool) -> Void, requestTonWithdraw: @escaping () -> Void, requestStarsWithdraw: @escaping () -> Void, openMonetizationIntro: @escaping () -> Void, openMonetizationInfo: @escaping () -> Void, openTonTransaction: @escaping (RevenueStatsTransactionsContext.State.Transaction) -> Void, openStarsTransaction: @escaping (StarsContext.State.Transaction) -> Void, expandTransactions: @escaping () -> Void, updateCpmEnabled: @escaping (Bool) -> Void, presentCpmLocked: @escaping () -> Void, dismissInput: @escaping () -> Void) {
self.context = context
self.loadDetailedGraph = loadDetailedGraph
self.openPostStats = openPostStats
@ -65,10 +69,13 @@ private final class ChannelStatsControllerArguments {
self.openGifts = openGifts
self.createPrepaidGiveaway = createPrepaidGiveaway
self.updateGiftsSelected = updateGiftsSelected
self.requestWithdraw = requestWithdraw
self.updateStarsSelected = updateStarsSelected
self.requestTonWithdraw = requestTonWithdraw
self.requestStarsWithdraw = requestStarsWithdraw
self.openMonetizationIntro = openMonetizationIntro
self.openMonetizationInfo = openMonetizationInfo
self.openTransaction = openTransaction
self.openTonTransaction = openTonTransaction
self.openStarsTransaction = openStarsTransaction
self.expandTransactions = expandTransactions
self.updateCpmEnabled = updateCpmEnabled
self.presentCpmLocked = presentCpmLocked
@ -101,9 +108,11 @@ private enum StatsSection: Int32 {
case adsHeader
case adsImpressions
case adsRevenue
case adsTonRevenue
case adsStarsRevenue
case adsProceeds
case adsBalance
case adsTonBalance
case adsStarsBalance
case adsTransactions
case adsCpm
}
@ -218,18 +227,27 @@ private enum StatsEntry: ItemListNodeEntry {
case adsImpressionsTitle(PresentationTheme, String)
case adsImpressionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType)
case adsRevenueTitle(PresentationTheme, String)
case adsRevenueGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType, Double)
case adsTonRevenueTitle(PresentationTheme, String)
case adsTonRevenueGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType, Double)
case adsStarsRevenueTitle(PresentationTheme, String)
case adsStarsRevenueGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType, Double)
case adsProceedsTitle(PresentationTheme, String)
case adsProceedsOverview(PresentationTheme, RevenueStats, TelegramMediaFile?)
case adsProceedsOverview(PresentationTheme, RevenueStats, StarsRevenueStats?)
case adsBalanceTitle(PresentationTheme, String)
case adsBalance(PresentationTheme, RevenueStats, Bool, Bool, TelegramMediaFile?)
case adsBalanceInfo(PresentationTheme, String)
case adsTonBalanceTitle(PresentationTheme, String)
case adsTonBalance(PresentationTheme, RevenueStats, Bool, Bool)
case adsTonBalanceInfo(PresentationTheme, String)
case adsStarsBalanceTitle(PresentationTheme, String)
case adsStarsBalance(PresentationTheme, StarsRevenueStats, Bool, Bool)
case adsStarsBalanceInfo(PresentationTheme, String)
case adsTransactionsTitle(PresentationTheme, String)
case adsTransactionsTabs(PresentationTheme, String, String, Bool)
case adsTransaction(Int32, PresentationTheme, RevenueStatsTransactionsContext.State.Transaction)
case adsStarsTransaction(Int32, PresentationTheme, StarsContext.State.Transaction)
case adsTransactionsExpand(PresentationTheme, String)
case adsCpmToggle(PresentationTheme, String, Int32, Bool?)
@ -281,13 +299,17 @@ private enum StatsEntry: ItemListNodeEntry {
return StatsSection.adsHeader.rawValue
case .adsImpressionsTitle, .adsImpressionsGraph:
return StatsSection.adsImpressions.rawValue
case .adsRevenueTitle, .adsRevenueGraph:
return StatsSection.adsRevenue.rawValue
case .adsTonRevenueTitle, .adsTonRevenueGraph:
return StatsSection.adsTonRevenue.rawValue
case .adsStarsRevenueTitle, .adsStarsRevenueGraph:
return StatsSection.adsStarsRevenue.rawValue
case .adsProceedsTitle, .adsProceedsOverview:
return StatsSection.adsProceeds.rawValue
case .adsBalanceTitle, .adsBalance, .adsBalanceInfo:
return StatsSection.adsBalance.rawValue
case .adsTransactionsTitle, .adsTransaction, .adsTransactionsExpand:
case .adsTonBalanceTitle, .adsTonBalance, .adsTonBalanceInfo:
return StatsSection.adsTonBalance.rawValue
case .adsStarsBalanceTitle, .adsStarsBalance, .adsStarsBalanceInfo:
return StatsSection.adsStarsBalance.rawValue
case .adsTransactionsTitle, .adsTransactionsTabs, .adsTransaction, .adsStarsTransaction, .adsTransactionsExpand:
return StatsSection.adsTransactions.rawValue
case .adsCpmToggle, .adsCpmInfo:
return StatsSection.adsCpm.rawValue
@ -392,30 +414,44 @@ private enum StatsEntry: ItemListNodeEntry {
return 20001
case .adsImpressionsGraph:
return 20002
case .adsRevenueTitle:
case .adsTonRevenueTitle:
return 20003
case .adsRevenueGraph:
case .adsTonRevenueGraph:
return 20004
case .adsProceedsTitle:
case .adsStarsRevenueTitle:
return 20005
case .adsProceedsOverview:
case .adsStarsRevenueGraph:
return 20006
case .adsBalanceTitle:
case .adsProceedsTitle:
return 20007
case .adsBalance:
case .adsProceedsOverview:
return 20008
case .adsBalanceInfo:
case .adsTonBalanceTitle:
return 20009
case .adsTransactionsTitle:
case .adsTonBalance:
return 20010
case .adsTonBalanceInfo:
return 20011
case .adsStarsBalanceTitle:
return 20012
case .adsStarsBalance:
return 20013
case .adsStarsBalanceInfo:
return 20014
case .adsTransactionsTitle:
return 20015
case .adsTransactionsTabs:
return 20016
case let .adsTransaction(index, _, _):
return 20011 + index
return 20017 + index
case let .adsStarsTransaction(index, _, _):
return 30017 + index
case .adsTransactionsExpand:
return 30000
return 40000
case .adsCpmToggle:
return 30001
return 40001
case .adsCpmInfo:
return 30002
return 40002
}
}
@ -709,14 +745,26 @@ private enum StatsEntry: ItemListNodeEntry {
} else {
return false
}
case let .adsRevenueTitle(lhsTheme, lhsText):
if case let .adsRevenueTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
case let .adsTonRevenueTitle(lhsTheme, lhsText):
if case let .adsTonRevenueTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .adsRevenueGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType, lhsRate):
if case let .adsRevenueGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType, rhsRate) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType, lhsRate == rhsRate {
case let .adsTonRevenueGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType, lhsRate):
if case let .adsTonRevenueGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType, rhsRate) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType, lhsRate == rhsRate {
return true
} else {
return false
}
case let .adsStarsRevenueTitle(lhsTheme, lhsText):
if case let .adsStarsRevenueTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .adsStarsRevenueGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType, lhsRate):
if case let .adsStarsRevenueGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType, rhsRate) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType, lhsRate == rhsRate {
return true
} else {
return false
@ -727,26 +775,44 @@ private enum StatsEntry: ItemListNodeEntry {
} else {
return false
}
case let .adsProceedsOverview(lhsTheme, lhsStatus, lhsAnimatedEmoji):
if case let .adsProceedsOverview(rhsTheme, rhsStatus, rhsAnimatedEmoji) = rhs, lhsTheme === rhsTheme, lhsStatus == rhsStatus, lhsAnimatedEmoji == rhsAnimatedEmoji {
case let .adsProceedsOverview(lhsTheme, lhsStatus, lhsStarsStatus):
if case let .adsProceedsOverview(rhsTheme, rhsStatus, rhsStarsStatus) = rhs, lhsTheme === rhsTheme, lhsStatus == rhsStatus, lhsStarsStatus == rhsStarsStatus {
return true
} else {
return false
}
case let .adsBalanceTitle(lhsTheme, lhsText):
if case let .adsBalanceTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
case let .adsTonBalanceTitle(lhsTheme, lhsText):
if case let .adsTonBalanceTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .adsBalance(lhsTheme, lhsStats, lhsCanWithdraw, lhsIsEnabled, lhsAnimatedEmoji):
if case let .adsBalance(rhsTheme, rhsStats, rhsCanWithdraw, rhsIsEnabled, rhsAnimatedEmoji) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsCanWithdraw == rhsCanWithdraw, lhsIsEnabled == rhsIsEnabled, lhsAnimatedEmoji == rhsAnimatedEmoji {
case let .adsTonBalance(lhsTheme, lhsStats, lhsCanWithdraw, lhsIsEnabled):
if case let .adsTonBalance(rhsTheme, rhsStats, rhsCanWithdraw, rhsIsEnabled) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsCanWithdraw == rhsCanWithdraw, lhsIsEnabled == rhsIsEnabled {
return true
} else {
return false
}
case let .adsBalanceInfo(lhsTheme, lhsText):
if case let .adsBalanceInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
case let .adsTonBalanceInfo(lhsTheme, lhsText):
if case let .adsTonBalanceInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .adsStarsBalanceTitle(lhsTheme, lhsText):
if case let .adsStarsBalanceTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .adsStarsBalance(lhsTheme, lhsStats, lhsCanWithdraw, lhsIsEnabled):
if case let .adsStarsBalance(rhsTheme, rhsStats, rhsCanWithdraw, rhsIsEnabled) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsCanWithdraw == rhsCanWithdraw, lhsIsEnabled == rhsIsEnabled {
return true
} else {
return false
}
case let .adsStarsBalanceInfo(lhsTheme, lhsText):
if case let .adsStarsBalanceInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
@ -757,12 +823,24 @@ private enum StatsEntry: ItemListNodeEntry {
} else {
return false
}
case let .adsTransactionsTabs(lhsTheme, lhsTonText, lhsStarsText, lhsStarsSelected):
if case let .adsTransactionsTabs(rhsTheme, rhsTonText, rhsStarsText, rhsStarsSelected) = rhs, lhsTheme === rhsTheme, lhsTonText == rhsTonText, lhsStarsText == rhsStarsText, lhsStarsSelected == rhsStarsSelected {
return true
} else {
return false
}
case let .adsTransaction(lhsIndex, lhsTheme, lhsTransaction):
if case let .adsTransaction(rhsIndex, rhsTheme, rhsTransaction) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTransaction == rhsTransaction {
return true
} else {
return false
}
case let .adsStarsTransaction(lhsIndex, lhsTheme, lhsTransaction):
if case let .adsStarsTransaction(rhsIndex, rhsTheme, rhsTransaction) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTransaction == rhsTransaction {
return true
} else {
return false
}
case let .adsTransactionsExpand(lhsTheme, lhsText):
if case let .adsTransactionsExpand(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
@ -811,9 +889,11 @@ private enum StatsEntry: ItemListNodeEntry {
let .boostersTitle(_, text),
let .boostLinkTitle(_, text),
let .adsImpressionsTitle(_, text),
let .adsRevenueTitle(_, text),
let .adsTonRevenueTitle(_, text),
let .adsStarsRevenueTitle(_, text),
let .adsProceedsTitle(_, text),
let .adsBalanceTitle(_, text),
let .adsTonBalanceTitle(_, text),
let .adsStarsBalanceTitle(_, text),
let .adsTransactionsTitle(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .boostPrepaidInfo(_, text),
@ -835,7 +915,9 @@ private enum StatsEntry: ItemListNodeEntry {
let .storyReactionsByEmotionGraph(_, _, _, graph, type),
let .adsImpressionsGraph(_, _, _, graph, type):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
case let .adsRevenueGraph(_, _, _, graph, type, rate):
case let .adsTonRevenueGraph(_, _, _, graph, type, rate):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, conversionRate: rate, sectionId: self.section, style: .blocks)
case let .adsStarsRevenueGraph(_, _, _, graph, type, rate):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, conversionRate: rate, sectionId: self.section, style: .blocks)
case let .postInteractionsGraph(_, _, _, graph, type),
let .instantPageInteractionsGraph(_, _, _, graph, type),
@ -959,9 +1041,9 @@ private enum StatsEntry: ItemListNodeEntry {
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in
arguments.openMonetizationIntro()
})
case let .adsProceedsOverview(_, stats, animatedEmoji):
return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: false, stats: stats, animatedEmoji: animatedEmoji, sectionId: self.section, style: .blocks)
case let .adsBalance(_, stats, canWithdraw, isEnabled, _):
case let .adsProceedsOverview(_, stats, starsStats):
return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: false, stats: stats, additionalStats: starsStats, sectionId: self.section, style: .blocks)
case let .adsTonBalance(_, stats, canWithdraw, isEnabled):
return MonetizationBalanceItem(
context: arguments.context,
presentationData: presentationData,
@ -969,15 +1051,36 @@ private enum StatsEntry: ItemListNodeEntry {
canWithdraw: canWithdraw,
isEnabled: isEnabled,
withdrawAction: {
arguments.requestWithdraw()
arguments.requestTonWithdraw()
},
sectionId: self.section,
style: .blocks
)
case let .adsBalanceInfo(_, text):
case let .adsTonBalanceInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in
arguments.openMonetizationInfo()
})
case let .adsStarsBalance(_, stats, canWithdraw, isEnabled):
return MonetizationBalanceItem(
context: arguments.context,
presentationData: presentationData,
stats: stats,
canWithdraw: canWithdraw,
isEnabled: isEnabled,
withdrawAction: {
arguments.requestStarsWithdraw()
},
sectionId: self.section,
style: .blocks
)
case let .adsStarsBalanceInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in
arguments.openMonetizationInfo()
})
case let .adsTransactionsTabs(_, tonText, starsText, starsSelected):
return BoostsTabsItem(theme: presentationData.theme, boostsText: tonText, giftsText: starsText, selectedTab: starsSelected ? .gifts : .boosts, sectionId: self.section, selectionUpdated: { tab in
arguments.updateStarsSelected(tab == .gifts)
})
case let .adsTransaction(_, theme, transaction):
let font = Font.with(size: floor(presentationData.fontSize.itemListBaseFontSize))
let smallLabelFont = Font.with(size: floor(presentationData.fontSize.itemListBaseFontSize / 17.0 * 13.0))
@ -1024,8 +1127,12 @@ private enum StatsEntry: ItemListNodeEntry {
}
return ItemListDisclosureItem(presentationData: presentationData, title: "", attributedTitle: title, label: "", attributedLabel: label, labelStyle: .coloredText(labelColor), additionalDetailLabel: detailText, additionalDetailLabelColor: detailColor, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
arguments.openTransaction(transaction)
arguments.openTonTransaction(transaction)
})
case let .adsStarsTransaction(_, _, transaction):
return StarsTransactionItem(context: arguments.context, presentationData: presentationData, transaction: transaction, action: {
arguments.openStarsTransaction(transaction)
}, sectionId: self.section, style: .blocks)
case let .adsTransactionsExpand(theme, title):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: {
arguments.expandTransactions()
@ -1062,23 +1169,26 @@ private struct ChannelStatsControllerState: Equatable {
let boostersExpanded: Bool
let moreBoostersDisplayed: Int32
let giftsSelected: Bool
let starsSelected: Bool
let transactionsExpanded: Bool
let moreTransactionsDisplayed: Int32
init() {
self.section = .stats
self.boostersExpanded = false
self.moreBoostersDisplayed = 0
self.giftsSelected = false
self.starsSelected = false
self.transactionsExpanded = false
self.moreTransactionsDisplayed = 0
}
init(section: ChannelStatsSection, boostersExpanded: Bool, moreBoostersDisplayed: Int32, giftsSelected: Bool, transactionsExpanded: Bool, moreTransactionsDisplayed: Int32) {
init(section: ChannelStatsSection, boostersExpanded: Bool, moreBoostersDisplayed: Int32, giftsSelected: Bool, starsSelected: Bool, transactionsExpanded: Bool, moreTransactionsDisplayed: Int32) {
self.section = section
self.boostersExpanded = boostersExpanded
self.moreBoostersDisplayed = moreBoostersDisplayed
self.giftsSelected = giftsSelected
self.starsSelected = starsSelected
self.transactionsExpanded = transactionsExpanded
self.moreTransactionsDisplayed = moreTransactionsDisplayed
}
@ -1096,6 +1206,9 @@ private struct ChannelStatsControllerState: Equatable {
if lhs.giftsSelected != rhs.giftsSelected {
return false
}
if lhs.starsSelected != rhs.starsSelected {
return false
}
if lhs.transactionsExpanded != rhs.transactionsExpanded {
return false
}
@ -1106,27 +1219,31 @@ private struct ChannelStatsControllerState: Equatable {
}
func withUpdatedSection(_ section: ChannelStatsSection) -> ChannelStatsControllerState {
return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
}
func withUpdatedBoostersExpanded(_ boostersExpanded: Bool) -> ChannelStatsControllerState {
return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
}
func withUpdatedMoreBoostersDisplayed(_ moreBoostersDisplayed: Int32) -> ChannelStatsControllerState {
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: moreBoostersDisplayed, giftsSelected: self.giftsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
}
func withUpdatedGiftsSelected(_ giftsSelected: Bool) -> ChannelStatsControllerState {
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: giftsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
}
func withUpdatedStarsSelected(_ giftsSelected: Bool) -> ChannelStatsControllerState {
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
}
func withUpdatedTransactionsExpanded(_ transactionsExpanded: Bool) -> ChannelStatsControllerState {
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, transactionsExpanded: transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed)
}
func withUpdatedMoreTransactionsDisplayed(_ moreTransactionsDisplayed: Int32) -> ChannelStatsControllerState {
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: moreTransactionsDisplayed)
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: moreTransactionsDisplayed)
}
}
@ -1373,13 +1490,12 @@ private func monetizationEntries(
data: RevenueStats,
boostData: ChannelBoostStatus?,
transactionsInfo: RevenueStatsTransactionsContext.State,
starsData: StarsRevenueStats?,
starsTransactionsInfo: StarsTransactionsContext.State,
adsRestricted: Bool,
animatedEmojis: [String: [StickerPackItem]],
premiumConfiguration: PremiumConfiguration,
monetizationConfiguration: MonetizationConfiguration
) -> [StatsEntry] {
let diamond = animatedEmojis["💎"]?.first?.file
var entries: [StatsEntry] = []
entries.append(.adsHeader(presentationData.theme, presentationData.strings.Monetization_Header))
@ -1389,19 +1505,24 @@ private func monetizationEntries(
}
if !data.revenueGraph.isEmpty {
entries.append(.adsRevenueTitle(presentationData.theme, presentationData.strings.Monetization_AdRevenueTitle))
entries.append(.adsRevenueGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.revenueGraph, .currency, data.usdRate))
entries.append(.adsTonRevenueTitle(presentationData.theme, presentationData.strings.Monetization_AdRevenueTitle))
entries.append(.adsTonRevenueGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.revenueGraph, .currency, data.usdRate))
}
if let starsData, !starsData.revenueGraph.isEmpty {
entries.append(.adsStarsRevenueTitle(presentationData.theme, presentationData.strings.Monetization_StarsRevenueTitle))
entries.append(.adsStarsRevenueGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, starsData.revenueGraph, .stars, starsData.usdRate))
}
entries.append(.adsProceedsTitle(presentationData.theme, presentationData.strings.Monetization_OverviewTitle))
entries.append(.adsProceedsOverview(presentationData.theme, data, diamond))
entries.append(.adsProceedsOverview(presentationData.theme, data, starsData))
var isCreator = false
if let peer, case let .channel(channel) = peer, channel.flags.contains(.isCreator) {
isCreator = true
}
entries.append(.adsBalanceTitle(presentationData.theme, presentationData.strings.Monetization_BalanceTitle))
entries.append(.adsBalance(presentationData.theme, data, isCreator && data.balances.availableBalance > 0, monetizationConfiguration.withdrawalAvailable, diamond))
entries.append(.adsTonBalanceTitle(presentationData.theme, presentationData.strings.Monetization_TonBalanceTitle))
entries.append(.adsTonBalance(presentationData.theme, data, isCreator && data.balances.availableBalance > 0, monetizationConfiguration.withdrawalAvailable))
if isCreator {
let withdrawalInfoText: String
@ -1412,11 +1533,35 @@ private func monetizationEntries(
} else {
withdrawalInfoText = presentationData.strings.Monetization_Balance_ComingLaterInfo
}
entries.append(.adsBalanceInfo(presentationData.theme, withdrawalInfoText))
entries.append(.adsTonBalanceInfo(presentationData.theme, withdrawalInfoText))
}
if !transactionsInfo.transactions.isEmpty {
entries.append(.adsTransactionsTitle(presentationData.theme, presentationData.strings.Monetization_TransactionsTitle))
if let starsData, starsData.balances.overallRevenue > 0 {
entries.append(.adsStarsBalanceTitle(presentationData.theme, presentationData.strings.Monetization_StarsBalanceTitle))
entries.append(.adsStarsBalance(presentationData.theme, starsData, isCreator && starsData.balances.availableBalance > 0, starsData.balances.withdrawEnabled))
entries.append(.adsStarsBalanceInfo(presentationData.theme, presentationData.strings.Monetization_Balance_StarsInfo))
}
var addedTransactionsTabs = false
if !transactionsInfo.transactions.isEmpty && !starsTransactionsInfo.transactions.isEmpty {
addedTransactionsTabs = true
entries.append(.adsTransactionsTabs(presentationData.theme, presentationData.strings.Monetization_TonTransactions, presentationData.strings.Monetization_StarsTransactions, state.starsSelected))
}
var displayTonTransactions = false
if !transactionsInfo.transactions.isEmpty && (starsTransactionsInfo.transactions.isEmpty || !state.starsSelected) {
displayTonTransactions = true
}
var displayStarsTransactions = false
if !starsTransactionsInfo.transactions.isEmpty && (transactionsInfo.transactions.isEmpty || state.starsSelected) {
displayStarsTransactions = true
}
if displayTonTransactions {
if !addedTransactionsTabs {
entries.append(.adsTransactionsTitle(presentationData.theme, presentationData.strings.Monetization_TonTransactions.uppercased()))
}
var transactions = transactionsInfo.transactions
var limit: Int32
@ -1438,7 +1583,38 @@ private func monetizationEntries(
if !state.transactionsExpanded {
moreCount = min(20, transactionsInfo.count - Int32(transactions.count))
} else {
moreCount = min(500, transactionsInfo.count - Int32(transactions.count))
moreCount = min(50, transactionsInfo.count - Int32(transactions.count))
}
entries.append(.adsTransactionsExpand(presentationData.theme, presentationData.strings.Monetization_Transaction_ShowMoreTransactions(moreCount)))
}
}
if displayStarsTransactions {
if !addedTransactionsTabs {
entries.append(.adsTransactionsTitle(presentationData.theme, presentationData.strings.Monetization_StarsTransactions.uppercased()))
}
var transactions = starsTransactionsInfo.transactions
var limit: Int32
if state.transactionsExpanded {
limit = 25 + state.moreTransactionsDisplayed
} else {
limit = initialTransactionsDisplayedLimit
}
transactions = Array(transactions.prefix(Int(limit)))
var i: Int32 = 0
for transaction in transactions {
entries.append(.adsStarsTransaction(i, presentationData.theme, transaction))
i += 1
}
if starsTransactionsInfo.canLoadMore || starsTransactionsInfo.transactions.count > transactions.count {
let moreCount: Int32
if !state.transactionsExpanded {
moreCount = min(20, Int32(starsTransactionsInfo.transactions.count - transactions.count))
} else {
moreCount = min(50, Int32(starsTransactionsInfo.transactions.count - transactions.count))
}
entries.append(.adsTransactionsExpand(presentationData.theme, presentationData.strings.Monetization_Transaction_ShowMoreTransactions(moreCount)))
}
@ -1471,9 +1647,10 @@ private func channelStatsControllerEntries(
giveawayAvailable: Bool,
isGroup: Bool,
boostsOnly: Bool,
animatedEmojis: [String: [StickerPackItem]],
revenueState: RevenueStats?,
revenueTransactions: RevenueStatsTransactionsContext.State,
starsState: StarsRevenueStats?,
starsTransactions: StarsTransactionsContext.State,
adsRestricted: Bool,
premiumConfiguration: PremiumConfiguration,
monetizationConfiguration: MonetizationConfiguration
@ -1512,8 +1689,9 @@ private func channelStatsControllerEntries(
data: revenueState,
boostData: boostData,
transactionsInfo: revenueTransactions,
starsData: starsState,
starsTransactionsInfo: starsTransactions,
adsRestricted: adsRestricted,
animatedEmojis: animatedEmojis,
premiumConfiguration: premiumConfiguration,
monetizationConfiguration: monetizationConfiguration
)
@ -1523,8 +1701,8 @@ private func channelStatsControllerEntries(
}
public func channelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, section: ChannelStatsSection = .stats, boostStatus: ChannelBoostStatus? = nil, boostStatusUpdated: ((ChannelBoostStatus) -> Void)? = nil) -> ViewController {
let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0), ignoreRepeated: true)
let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0))
let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, starsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0), ignoreRepeated: true)
let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, starsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0))
let updateState: ((ChannelStatsControllerState) -> ChannelStatsControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
@ -1583,7 +1761,13 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
let revenueState = Promise<RevenueStatsContextState?>()
revenueState.set(.single(nil) |> then(revenueContext.state |> map(Optional.init)))
let starsContext = context.engine.payments.peerStarsRevenueContext(peerId: peerId)
let starsState = Promise<StarsRevenueStatsContextState?>()
starsState.set(.single(nil) |> then(starsContext.state |> map(Optional.init)))
let revenueTransactions = RevenueStatsTransactionsContext(account: context.account, peerId: peerId)
let starsTransactions = context.engine.payments.peerStarsTransactionsContext(subject: .peer(peerId), mode: .all)
starsTransactions.loadMore()
var dismissAllTooltipsImpl: (() -> Void)?
var presentImpl: ((ViewController) -> Void)?
@ -1592,8 +1776,10 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
var navigateToChatImpl: ((EnginePeer) -> Void)?
var navigateToMessageImpl: ((EngineMessage.Id) -> Void)?
var openBoostImpl: ((Bool) -> Void)?
var openTransactionImpl: ((RevenueStatsTransactionsContext.State.Transaction) -> Void)?
var requestWithdrawImpl: (() -> Void)?
var openTonTransactionImpl: ((RevenueStatsTransactionsContext.State.Transaction) -> Void)?
var openStarsTransactionImpl: ((StarsContext.State.Transaction) -> Void)?
var requestTonWithdrawImpl: (() -> Void)?
var requestStarsWithdrawImpl: (() -> Void)?
var updateStatusBarImpl: ((StatusBarStyle) -> Void)?
var dismissInputImpl: (() -> Void)?
@ -1716,8 +1902,14 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
updateGiftsSelected: { selected in
updateState { $0.withUpdatedGiftsSelected(selected).withUpdatedBoostersExpanded(false) }
},
requestWithdraw: {
requestWithdrawImpl?()
updateStarsSelected: { selected in
updateState { $0.withUpdatedStarsSelected(selected).withUpdatedTransactionsExpanded(false) }
},
requestTonWithdraw: {
requestTonWithdrawImpl?()
},
requestStarsWithdraw: {
requestStarsWithdrawImpl?()
},
openMonetizationIntro: {
let controller = MonetizationIntroScreen(context: context, openMore: {})
@ -1727,18 +1919,27 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: presentationData.strings.Monetization_BalanceInfo_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
},
openTransaction: { transaction in
openTransactionImpl?(transaction)
openTonTransaction: { transaction in
openTonTransactionImpl?(transaction)
},
openStarsTransaction: { transaction in
openStarsTransactionImpl?(transaction)
},
expandTransactions: {
var starsSelected = false
updateState { state in
starsSelected = state.starsSelected
if state.transactionsExpanded {
return state.withUpdatedMoreTransactionsDisplayed(state.moreTransactionsDisplayed + 50)
} else {
return state.withUpdatedTransactionsExpanded(true)
}
}
revenueTransactions.loadMore()
if starsSelected {
starsTransactions.loadMore()
} else {
revenueTransactions.loadMore()
}
},
updateCpmEnabled: { value in
let _ = context.engine.peers.updateChannelRestrictAdMessages(peerId: peerId, restricted: value).start()
@ -1802,12 +2003,13 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
giftsContext.state,
revenueState.get(),
revenueTransactions.state,
starsState.get(),
starsTransactions.state,
peerData,
longLoadingSignal,
context.animatedEmojiStickers
longLoadingSignal
)
|> deliverOnMainQueue
|> map { presentationData, state, peer, data, messageView, stories, boostData, boostersState, giftsState, revenueState, revenueTransactions, peerData, longLoading, animatedEmojiStickers -> (ItemListControllerState, (ItemListNodeState, Any)) in
|> map { presentationData, state, peer, data, messageView, stories, boostData, boostersState, giftsState, revenueState, revenueTransactions, starsState, starsTransactions, peerData, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in
let (adsRestricted, canViewRevenue) = peerData
var isGroup = false
@ -1900,7 +2102,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelStatsControllerEntries(presentationData: presentationData, state: state, peer: peer, data: data, messages: messages, stories: stories, interactions: interactions, boostData: boostData, boostersState: boostersState, giftsState: giftsState, giveawayAvailable: premiumConfiguration.giveawayGiftsPurchaseAvailable, isGroup: isGroup, boostsOnly: boostsOnly, animatedEmojis: animatedEmojiStickers, revenueState: revenueState?.stats, revenueTransactions: revenueTransactions, adsRestricted: adsRestricted, premiumConfiguration: premiumConfiguration, monetizationConfiguration: monetizationConfiguration), style: .blocks, emptyStateItem: emptyStateItem, headerItem: headerItem, crossfadeState: previous == nil, animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelStatsControllerEntries(presentationData: presentationData, state: state, peer: peer, data: data, messages: messages, stories: stories, interactions: interactions, boostData: boostData, boostersState: boostersState, giftsState: giftsState, giveawayAvailable: premiumConfiguration.giveawayGiftsPurchaseAvailable, isGroup: isGroup, boostsOnly: boostsOnly, revenueState: revenueState?.stats, revenueTransactions: revenueTransactions, starsState: starsState?.stats, starsTransactions: starsTransactions, adsRestricted: adsRestricted, premiumConfiguration: premiumConfiguration, monetizationConfiguration: monetizationConfiguration), style: .blocks, emptyStateItem: emptyStateItem, headerItem: headerItem, crossfadeState: previous == nil, animateChanges: false)
return (controllerState, (listState, arguments))
}
@ -1909,6 +2111,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
let _ = statsContext.state
let _ = storyList.state
let _ = revenueContext.state
let _ = starsContext.state
}
let controller = ItemListController(context: context, state: signal)
@ -2122,7 +2325,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
})
}
}
requestWithdrawImpl = {
requestTonWithdrawImpl = {
withdrawalDisposable.set((context.engine.peers.checkChannelRevenueWithdrawalAvailability()
|> deliverOnMainQueue).start(error: { error in
let controller = revenueWithdrawalController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, initialError: error, present: { c, _ in
@ -2134,7 +2337,44 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
presentImpl?(controller)
}))
}
openTransactionImpl = { transaction in
requestStarsWithdrawImpl = {
withdrawalDisposable.set((context.engine.peers.checkStarsRevenueWithdrawalAvailability()
|> deliverOnMainQueue).start(error: { error in
switch error {
case .requestPassword:
let _ = (starsContext.state
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { state in
guard let stats = state.stats else {
return
}
let controller = context.sharedContext.makeStarsWithdrawalScreen(context: context, stats: stats, completion: { amount in
let controller = confirmStarsRevenueWithdrawalController(context: context, peerId: peerId, amount: amount, present: { c, a in
presentImpl?(c)
}, completion: { url in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
Queue.mainQueue().after(2.0) {
starsContext.reload()
starsTransactions.reload()
}
})
presentImpl?(controller)
})
pushImpl?(controller)
})
default:
let controller = starsRevenueWithdrawalController(context: context, peerId: peerId, amount: 0, initialError: error, present: { c, a in
presentImpl?(c)
}, completion: { _ in
})
presentImpl?(controller)
}
}))
}
openTonTransactionImpl = { transaction in
let _ = (peer.get()
|> take(1)
|> deliverOnMainQueue).start(next: { peer in
@ -2146,6 +2386,16 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
}))
})
}
openStarsTransactionImpl = { transaction in
let _ = (peer.get()
|> take(1)
|> deliverOnMainQueue).start(next: { peer in
guard let peer else {
return
}
pushImpl?(context.sharedContext.makeStarsTransactionScreen(context: context, transaction: transaction, peer: peer))
})
}
updateStatusBarImpl = { [weak controller] style in
controller?.setStatusBarStyle(style, animated: true)
}

View file

@ -13,7 +13,7 @@ import TextFormat
final class MonetizationBalanceItem: ListViewItem, ItemListItem {
let context: AccountContext
let presentationData: ItemListPresentationData
let stats: RevenueStats
let stats: Stats
let canWithdraw: Bool
let isEnabled: Bool
let withdrawAction: () -> Void
@ -23,7 +23,7 @@ final class MonetizationBalanceItem: ListViewItem, ItemListItem {
init(
context: AccountContext,
presentationData: ItemListPresentationData,
stats: RevenueStats,
stats: Stats,
canWithdraw: Bool,
isEnabled: Bool,
withdrawAction: @escaping () -> Void,
@ -158,13 +158,24 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode {
let integralFont = Font.with(size: 48.0, design: .round, weight: .semibold)
let fractionalFont = Font.with(size: 24.0, design: .round, weight: .semibold)
let cryptoValue = formatBalanceText(item.stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator)
let amountString: NSAttributedString
let value: String
let amountString = amountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor)
var isStars = false
if let stats = item.stats as? RevenueStats {
let cryptoValue = formatBalanceText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator)
amountString = amountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor)
value = stats.balances.availableBalance == 0 ? "" : "\(formatUsdValue(stats.balances.availableBalance, rate: stats.usdRate))"
} else if let stats = item.stats as? StarsRevenueStats {
amountString = NSAttributedString(string: presentationStringsFormattedNumber(Int32(stats.balances.availableBalance), item.presentationData.dateTimeFormat.groupingSeparator), font: integralFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
value = stats.balances.availableBalance == 0 ? "" : "\(formatUsdValue(stats.balances.availableBalance, rate: stats.usdRate))"
isStars = true
} else {
fatalError()
}
let (balanceLayout, balanceApply) = makeBalanceTextLayout(TextNodeLayoutArguments(attributedString: amountString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let value = item.stats.balances.availableBalance == 0 ? "" : "\(formatUsdValue(item.stats.balances.availableBalance, rate: item.stats.usdRate))"
let (valueLayout, valueApply) = makeValueTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: value, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let verticalInset: CGFloat = 13.0
@ -272,7 +283,11 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode {
}
if themeUpdated {
strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonBig"), color: item.presentationData.theme.list.itemAccentColor)
if isStars {
strongSelf.iconNode.image = UIImage(bundleImageName: "Premium/Stars/BalanceStar")
} else {
strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonBig"), color: item.presentationData.theme.list.itemAccentColor)
}
}
var emojiItemSize = CGSize()
@ -304,7 +319,7 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.addSubnode(withdrawButtonNode)
strongSelf.withdrawButtonNode = withdrawButtonNode
}
withdrawButtonNode.title = item.presentationData.strings.Monetization_BalanceWithdraw
withdrawButtonNode.title = isStars ? item.presentationData.strings.Monetization_BalanceStarsWithdraw : item.presentationData.strings.Monetization_BalanceWithdraw
withdrawButtonNode.isEnabled = item.isEnabled
let buttonWidth = contentSize.width - leftInset - rightInset

View file

@ -0,0 +1,395 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import AccountContext
import TelegramPresentationData
import ItemListUI
import ComponentFlow
import ListActionItemComponent
import MultilineTextComponent
import TelegramStringFormatting
import StarsAvatarComponent
final class StarsTransactionItem: ListViewItem, ItemListItem {
let context: AccountContext
let presentationData: ItemListPresentationData
let transaction: StarsContext.State.Transaction
let action: () -> Void
let sectionId: ItemListSectionId
let style: ItemListStyle
init(
context: AccountContext,
presentationData: ItemListPresentationData,
transaction: StarsContext.State.Transaction,
action: @escaping () -> Void,
sectionId: ItemListSectionId,
style: ItemListStyle
) {
self.context = context
self.presentationData = presentationData
self.transaction = transaction
self.action = action
self.sectionId = sectionId
self.style = style
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = StarsTransactionItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? StarsTransactionItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
var selectable: Bool = true
public func selected(listView: ListView) {
listView.clearHighlightAnimated(true)
self.action()
}
}
final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
private let componentView: ComponentView<Empty>
private let activateArea: AccessibilityAreaNode
private var item: StarsTransactionItem?
var tag: ItemListItemTag? = nil
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.maskNode = ASImageNode()
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.componentView = ComponentView<Empty>()
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false)
}
func asyncLayout() -> (_ item: StarsTransactionItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
let leftInset = 16.0 + params.leftInset
let height: CGFloat = 78.0
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = .clear
insets = UIEdgeInsets()
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
contentSize = CGSize(width: params.width, height: height)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityTraits = []
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
let fontBaseDisplaySize = 17.0
let itemTitle: String
let itemSubtitle: String?
var itemDate: String
switch item.transaction.peer {
case let .peer(peer):
if !item.transaction.media.isEmpty {
//TODO:localize
itemTitle = "Media Purchase"
itemSubtitle = peer.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast)
} else if let title = item.transaction.title {
itemTitle = title
itemSubtitle = peer.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast)
} else {
itemTitle = peer.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast)
itemSubtitle = nil
}
case .appStore:
itemTitle = item.presentationData.strings.Stars_Intro_Transaction_AppleTopUp_Title
itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_AppleTopUp_Subtitle
case .playMarket:
itemTitle = item.presentationData.strings.Stars_Intro_Transaction_GoogleTopUp_Title
itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_GoogleTopUp_Subtitle
case .fragment:
itemTitle = item.presentationData.strings.Stars_Intro_Transaction_FragmentWithdrawal_Title
itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_FragmentWithdrawal_Subtitle
case .premiumBot:
itemTitle = item.presentationData.strings.Stars_Intro_Transaction_PremiumBotTopUp_Title
itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_PremiumBotTopUp_Subtitle
case .unsupported:
itemTitle = item.presentationData.strings.Stars_Intro_Transaction_Unsupported_Title
itemSubtitle = nil
}
let itemLabel: NSAttributedString
let labelString: String
let formattedLabel = presentationStringsFormattedNumber(abs(Int32(item.transaction.count)), item.presentationData.dateTimeFormat.groupingSeparator)
if item.transaction.count < 0 {
labelString = "- \(formattedLabel)"
} else {
labelString = "+ \(formattedLabel)"
}
itemLabel = NSAttributedString(string: labelString, font: Font.medium(fontBaseDisplaySize), textColor: labelString.hasPrefix("-") ? item.presentationData.theme.list.itemDestructiveColor : item.presentationData.theme.list.itemDisclosureActions.constructive.fillColor)
itemDate = stringForMediumCompactDate(timestamp: item.transaction.date, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
if item.transaction.flags.contains(.isRefund) {
itemDate += " \(item.presentationData.strings.Stars_Intro_Transaction_Refund)"
}
var titleComponents: [AnyComponentWithIdentity<Empty>] = []
titleComponents.append(
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: itemTitle,
font: Font.semibold(fontBaseDisplaySize),
textColor: item.presentationData.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)))
)
if let itemSubtitle {
titleComponents.append(
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: itemSubtitle,
font: Font.regular(fontBaseDisplaySize * 16.0 / 17.0),
textColor: item.presentationData.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)))
)
}
titleComponents.append(
AnyComponentWithIdentity(id: AnyHashable(2), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: itemDate,
font: Font.regular(floor(fontBaseDisplaySize * 14.0 / 17.0)),
textColor: item.presentationData.theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 1
)))
)
let itemSize = strongSelf.componentView.update(
transition: .immediate,
component: AnyComponent(ListActionItemComponent(
theme: item.presentationData.theme,
title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)),
contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0),
leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: item.context, theme: item.presentationData.theme, peer: item.transaction.peer, photo: item.transaction.photo, media: item.transaction.media))), false),
icon: nil,
accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))),
action: { [weak self] _ in
guard let self, let item = self.item else {
return
}
if !item.transaction.flags.contains(.isLocal) {
item.action()
}
}
)),
environment: {},
containerSize: CGSize(width: params.width - params.leftInset - params.rightInset, height: height)
)
let itemFrame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: itemSize)
if let itemComponentView = strongSelf.componentView.view {
if itemComponentView.superview == nil {
strongSelf.view.addSubview(itemComponentView)
}
itemComponentView.isUserInteractionEnabled = false
itemComponentView.frame = itemFrame
}
}
})
}
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

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)
@ -214,7 +225,27 @@ private final class ValueItemNode: ASDisplayNode {
let _ = deltaApply()
var valueOffset: CGFloat = 0.0
if isTon {
let iconName: String?
var iconTinted = false
switch mode {
case .ton:
iconName = "Ads/TonMedium"
iconTinted = true
valueOffset = 17.0
case .stars:
iconName = "Premium/Stars/StarMedium"
valueOffset = 19.0
default:
iconName = nil
}
var iconNameUpdated = false
if targetNode.currentIconName != iconName {
targetNode.currentIconName = iconName
iconNameUpdated = true
}
if let iconName {
let iconNode: ASImageNode
if let current = targetNode.iconNode {
iconNode = current
@ -225,16 +256,18 @@ private final class ValueItemNode: ASDisplayNode {
targetNode.addSubnode(iconNode)
}
if themeUpdated {
iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonMedium"), color: presentationData.theme.list.itemAccentColor)
if themeUpdated || iconNameUpdated {
if iconTinted {
iconNode.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: presentationData.theme.list.itemAccentColor)
} else {
iconNode.image = UIImage(bundleImageName: iconName)
}
}
if let icon = iconNode.image {
let iconFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((valueLayout.size.height - icon.size.height) / 2.0) - 1.0), size: icon.size)
iconNode.frame = iconFrame
}
valueOffset += 17.0
} else if let iconNode = targetNode.iconNode {
iconNode.removeFromSupernode()
targetNode.iconNode = nil
@ -382,7 +415,7 @@ class StatsOverviewItemNode: ListViewItemNode {
compactNumericCountString(stats.views),
item.presentationData.strings.Stats_Message_Views,
nil,
false
.generic
)
topRightItemLayoutAndApply = makeTopRightItemLayout(
@ -392,7 +425,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "",
item.presentationData.strings.Stats_Message_PublicShares,
nil,
false
.generic
)
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
@ -402,7 +435,7 @@ class StatsOverviewItemNode: ListViewItemNode {
compactNumericCountString(stats.reactions),
item.presentationData.strings.Stats_Message_Reactions,
nil,
false
.generic
)
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
@ -412,7 +445,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.publicShares.flatMap { "\( compactNumericCountString(max(0, stats.forwards - Int($0))))" } ?? "",
item.presentationData.strings.Stats_Message_PrivateShares,
nil,
false
.generic
)
height += topRightItemLayoutAndApply!.0.height * 2.0 + verticalSpacing
@ -424,7 +457,7 @@ class StatsOverviewItemNode: ListViewItemNode {
compactNumericCountString(views.seenCount),
item.presentationData.strings.Stats_Message_Views,
nil,
false
.generic
)
topRightItemLayoutAndApply = makeTopRightItemLayout(
@ -434,7 +467,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "",
item.presentationData.strings.Stats_Message_PublicShares,
nil,
false
.generic
)
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
@ -444,7 +477,7 @@ class StatsOverviewItemNode: ListViewItemNode {
compactNumericCountString(views.reactedCount),
item.presentationData.strings.Stats_Message_Reactions,
nil,
false
.generic
)
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
@ -454,7 +487,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.publicShares.flatMap { "\( compactNumericCountString(max(0, views.forwardCount - Int($0))))" } ?? "",
item.presentationData.strings.Stats_Message_PrivateShares,
nil,
false
.generic
)
height += topRightItemLayoutAndApply!.0.height * 2.0 + verticalSpacing
@ -466,7 +499,7 @@ class StatsOverviewItemNode: ListViewItemNode {
"\(stats.level)",
item.presentationData.strings.Stats_Boosts_Level,
nil,
false
.generic
)
var premiumSubscribers: Double = 0.0
@ -481,7 +514,7 @@ class StatsOverviewItemNode: ListViewItemNode {
"\(Int(stats.premiumAudience?.value ?? 0))",
item.isGroup ? item.presentationData.strings.Stats_Boosts_PremiumMembers : item.presentationData.strings.Stats_Boosts_PremiumSubscribers,
(String(format: "%.02f%%", premiumSubscribers * 100.0), .generic),
false
.generic
)
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
@ -491,7 +524,7 @@ class StatsOverviewItemNode: ListViewItemNode {
"\(stats.boosts)",
item.presentationData.strings.Stats_Boosts_ExistingBoosts,
nil,
false
.generic
)
let boostsLeft: Int32
@ -507,7 +540,7 @@ class StatsOverviewItemNode: ListViewItemNode {
"\(boostsLeft)",
item.presentationData.strings.Stats_Boosts_BoostsToLevelUp,
nil,
false
.generic
)
if twoColumnLayout {
@ -532,7 +565,7 @@ class StatsOverviewItemNode: ListViewItemNode {
compactNumericCountString(Int(stats.followers.current)),
item.presentationData.strings.Stats_Followers,
(followersDelta.text, followersDelta.positive ? .positive : .negative),
false
.generic
)
var enabledNotifications: Double = 0.0
@ -546,7 +579,7 @@ class StatsOverviewItemNode: ListViewItemNode {
String(format: "%.02f%%", enabledNotifications * 100.0),
item.presentationData.strings.Stats_EnabledNotifications,
nil,
false
.generic
)
let hasMessages = stats.viewsPerPost.current > 0 || viewsPerPostDelta.hasValue
@ -605,7 +638,7 @@ class StatsOverviewItemNode: ListViewItemNode {
value,
title,
delta,
false
.generic
)
}
if let (value, title, delta) = items[1] {
@ -616,7 +649,7 @@ class StatsOverviewItemNode: ListViewItemNode {
value,
title,
delta,
false
.generic
)
}
if let (value, title, delta) = items[2] {
@ -627,7 +660,7 @@ class StatsOverviewItemNode: ListViewItemNode {
value,
title,
delta,
false
.generic
)
}
if let (value, title, delta) = items[3] {
@ -638,7 +671,7 @@ class StatsOverviewItemNode: ListViewItemNode {
value,
title,
delta,
false
.generic
)
}
if let (value, title, delta) = items[4] {
@ -649,7 +682,7 @@ class StatsOverviewItemNode: ListViewItemNode {
value,
title,
delta,
false
.generic
)
}
if let (value, title, delta) = items[5] {
@ -660,7 +693,7 @@ class StatsOverviewItemNode: ListViewItemNode {
value,
title,
delta,
false
.generic
)
}
@ -684,7 +717,7 @@ class StatsOverviewItemNode: ListViewItemNode {
compactNumericCountString(Int(stats.members.current)),
item.presentationData.strings.Stats_GroupMembers,
(membersDelta.text, membersDelta.positive ? .positive : .negative),
false
.generic
)
let messagesDelta = deltaText(stats.messages)
@ -695,7 +728,7 @@ class StatsOverviewItemNode: ListViewItemNode {
compactNumericCountString(Int(stats.messages.current)),
item.presentationData.strings.Stats_GroupMessages,
(messagesDelta.text, messagesDelta.positive ? .positive : .negative),
false
.generic
)
if displayBottomRow {
@ -706,7 +739,7 @@ class StatsOverviewItemNode: ListViewItemNode {
compactNumericCountString(Int(stats.viewers.current)),
item.presentationData.strings.Stats_GroupViewers,
(viewersDelta.text, viewersDelta.positive ? .positive : .negative),
false
.generic
)
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
@ -716,7 +749,7 @@ class StatsOverviewItemNode: ListViewItemNode {
compactNumericCountString(Int(stats.posters.current)),
item.presentationData.strings.Stats_GroupPosters,
(postersDelta.text, postersDelta.positive ? .positive : .negative),
false
.generic
)
}
@ -726,39 +759,105 @@ class StatsOverviewItemNode: ListViewItemNode {
height += topLeftItemLayoutAndApply!.0.height * 4.0 + verticalSpacing * 3.0
}
} else if let stats = item.stats as? RevenueStats {
twoColumnLayout = false
topLeftItemLayoutAndApply = makeTopLeftItemLayout(
item.context,
params.width,
item.presentationData,
formatBalanceText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator),
item.presentationData.strings.Monetization_Overview_Available,
(stats.balances.availableBalance == 0 ? "" : "\(formatUsdValue(stats.balances.availableBalance, rate: stats.usdRate))", .generic),
true
)
topRightItemLayoutAndApply = makeTopRightItemLayout(
item.context,
params.width,
item.presentationData,
formatBalanceText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator),
item.presentationData.strings.Monetization_Overview_Current,
(stats.balances.currentBalance == 0 ? "" : "\(formatUsdValue(stats.balances.currentBalance, rate: stats.usdRate))", .generic),
true
)
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
item.context,
params.width,
item.presentationData,
formatBalanceText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator),
item.presentationData.strings.Monetization_Overview_Total,
(stats.balances.overallRevenue == 0 ? "" : "\(formatUsdValue(stats.balances.overallRevenue, rate: stats.usdRate))", .generic),
true
)
height += topLeftItemLayoutAndApply!.0.height * 3.0 + verticalSpacing * 2.0
if let additionalStats = item.additionalStats as? StarsRevenueStats, additionalStats.balances.overallRevenue > 0 {
twoColumnLayout = true
topLeftItemLayoutAndApply = makeTopLeftItemLayout(
item.context,
params.width,
item.presentationData,
formatBalanceText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator),
"Available Balance", //item.presentationData.strings.Monetization_Overview_Available,
(stats.balances.availableBalance == 0 ? "" : "\(formatUsdValue(stats.balances.availableBalance, rate: stats.usdRate))", .generic),
.ton
)
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
item.context,
params.width,
item.presentationData,
formatBalanceText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator),
"Current Balance", //item.presentationData.strings.Monetization_Overview_Current,
(stats.balances.currentBalance == 0 ? "" : "\(formatUsdValue(stats.balances.currentBalance, rate: stats.usdRate))", .generic),
.ton
)
middle2LeftItemLayoutAndApply = makeMiddle2LeftItemLayout(
item.context,
params.width,
item.presentationData,
formatBalanceText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator),
"Lifetime Proceeds",//item.presentationData.strings.Monetization_Overview_Total,
(stats.balances.overallRevenue == 0 ? "" : "\(formatUsdValue(stats.balances.overallRevenue, rate: stats.usdRate))", .generic),
.ton
)
topRightItemLayoutAndApply = makeTopRightItemLayout(
item.context,
params.width,
item.presentationData,
presentationStringsFormattedNumber(Int32(additionalStats.balances.availableBalance), item.presentationData.dateTimeFormat.groupingSeparator),
" ",
(additionalStats.balances.availableBalance == 0 ? "" : "\(formatUsdValue(additionalStats.balances.availableBalance, rate: additionalStats.usdRate))", .generic),
.stars
)
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
item.context,
params.width,
item.presentationData,
presentationStringsFormattedNumber(Int32(additionalStats.balances.currentBalance), item.presentationData.dateTimeFormat.groupingSeparator),
" ",
(additionalStats.balances.currentBalance == 0 ? "" : "\(formatUsdValue(additionalStats.balances.currentBalance, rate: additionalStats.usdRate))", .generic),
.stars
)
middle2RightItemLayoutAndApply = makeMiddle2RightItemLayout(
item.context,
params.width,
item.presentationData,
presentationStringsFormattedNumber(Int32(additionalStats.balances.overallRevenue), item.presentationData.dateTimeFormat.groupingSeparator),
" ",
(additionalStats.balances.overallRevenue == 0 ? "" : "\(formatUsdValue(additionalStats.balances.overallRevenue, rate: additionalStats.usdRate))", .generic),
.stars
)
height += topLeftItemLayoutAndApply!.0.height * 3.0 + verticalSpacing * 2.0
} else {
twoColumnLayout = false
topLeftItemLayoutAndApply = makeTopLeftItemLayout(
item.context,
params.width,
item.presentationData,
formatBalanceText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator),
item.presentationData.strings.Monetization_Overview_Available,
(stats.balances.availableBalance == 0 ? "" : "\(formatUsdValue(stats.balances.availableBalance, rate: stats.usdRate))", .generic),
.ton
)
topRightItemLayoutAndApply = makeTopRightItemLayout(
item.context,
params.width,
item.presentationData,
formatBalanceText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator),
item.presentationData.strings.Monetization_Overview_Current,
(stats.balances.currentBalance == 0 ? "" : "\(formatUsdValue(stats.balances.currentBalance, rate: stats.usdRate))", .generic),
.ton
)
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
item.context,
params.width,
item.presentationData,
formatBalanceText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator),
item.presentationData.strings.Monetization_Overview_Total,
(stats.balances.overallRevenue == 0 ? "" : "\(formatUsdValue(stats.balances.overallRevenue, rate: stats.usdRate))", .generic),
.ton
)
height += topLeftItemLayoutAndApply!.0.height * 3.0 + verticalSpacing * 2.0
}
}
let contentSize = CGSize(width: params.width, height: height)

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)
}
}
@ -236,7 +236,7 @@ private final class StarsContextImpl {
}
private extension StarsContext.State.Transaction {
init?(apiTransaction: Api.StarsTransaction, transaction: Transaction) {
init?(apiTransaction: Api.StarsTransaction, peerId: EnginePeer.Id?, transaction: Transaction) {
switch apiTransaction {
case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia):
let parsedPeer: StarsContext.State.Transaction.Peer
@ -258,7 +258,11 @@ private extension StarsContext.State.Transaction {
}
parsedPeer = .peer(EnginePeer(peer))
if let messageId {
paidMessageId = MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: messageId)
if let peerId {
paidMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: messageId)
} else {
paidMessageId = MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: messageId)
}
}
}

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

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

@ -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
@ -134,7 +136,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
if let media = media as? TelegramMediaPaidContent {
var index = 0
for _ in media.extendedMedia {
result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(index: index, isAttachment: true, neighborType: .media, neighborSpacing: .default)))
result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(index: index, isAttachment: false, neighborType: .media, neighborSpacing: .default)))
index += 1
}
} else if let _ = media as? TelegramMediaImage {
@ -636,6 +638,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
private var actionButtonsNode: ChatMessageActionButtonsNode?
private var reactionButtonsNode: ChatMessageReactionButtonsNode?
private var unlockButtonNode: ChatMessageUnlockMediaNode?
private var mediaInfoNode: ChatMessageStarsMediaInfoNode?
private var shareButtonNode: ChatMessageShareButton?
private let messageAccessibilityArea: AccessibilityAreaNode
@ -680,6 +685,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
replyInfoNode.visibility = self.visibility != .none
}
if let unlockButtonNode = self.unlockButtonNode {
unlockButtonNode.visibility = self.visibility != .none
}
self.visibilityStatus = self.visibility != .none
self.updateVisibility()
@ -1198,6 +1207,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if let replyInfoNode = strongSelf.replyInfoNode, replyInfoNode.frame.contains(point) {
return .waitForSingleTap
}
if let unlockButtonNode = strongSelf.unlockButtonNode, unlockButtonNode.frame.contains(point) {
if let _ = unlockButtonNode.hitTest(strongSelf.view.convert(point, to: unlockButtonNode.view), with: nil) {
return .fail
}
}
if let forwardInfoNode = strongSelf.forwardInfoNode, forwardInfoNode.frame.contains(point) {
if forwardInfoNode.hasAction(at: strongSelf.view.convert(point, to: forwardInfoNode.view)) {
return .fail
@ -1366,6 +1380,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
let replyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode)
let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode)
let reactionButtonsLayout = ChatMessageReactionButtonsNode.asyncLayout(self.reactionButtonsNode)
let unlockButtonLayout = ChatMessageUnlockMediaNode.asyncLayout(self.unlockButtonNode)
let mediaInfoLayout = ChatMessageStarsMediaInfoNode.asyncLayout(self.mediaInfoNode)
let mosaicStatusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.mosaicStatusNode)
@ -1391,6 +1407,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
replyInfoLayout: replyInfoLayout,
actionButtonsLayout: actionButtonsLayout,
reactionButtonsLayout: reactionButtonsLayout,
unlockButtonLayout: unlockButtonLayout,
mediaInfoLayout: mediaInfoLayout,
mosaicStatusLayout: mosaicStatusLayout,
layoutConstants: layoutConstants,
currentItem: currentItem,
@ -1412,6 +1430,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
replyInfoLayout: (ChatMessageReplyInfoNode.Arguments) -> (CGSize, (CGSize, Bool, ListViewItemUpdateAnimation) -> ChatMessageReplyInfoNode),
actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, WallpaperBackgroundNode?, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)),
reactionButtonsLayout: (ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)),
unlockButtonLayout: (ChatMessageUnlockMediaNode.Arguments) -> (CGSize, (Bool) -> ChatMessageUnlockMediaNode),
mediaInfoLayout: (ChatMessageStarsMediaInfoNode.Arguments) -> (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode),
mosaicStatusLayout: (ChatMessageDateAndStatusNode.Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)),
layoutConstants: ChatMessageItemLayoutConstants,
currentItem: ChatMessageItem?,
@ -2185,7 +2205,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
if case .customChatContents = item.associatedData.subject {
} else if (mosaicRange.upperBound == contentPropertiesAndLayouts.count || contentPropertiesAndLayouts[contentPropertiesAndLayouts.count - 1].3.isAttachment) && !hasText {
} else if (mosaicRange.upperBound == contentPropertiesAndLayouts.count || contentPropertiesAndLayouts[contentPropertiesAndLayouts.count - 1].3.isAttachment) && (!hasText || item.message.invertMedia) {
let message = item.content.firstMessage
var edited = false
@ -2286,6 +2306,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
var forwardSource: Peer?
var forwardAuthorSignature: String?
var unlockButtonSizeApply: (CGSize, (Bool) -> ChatMessageUnlockMediaNode?) = (CGSize(), { _ in nil })
var mediaInfoSizeApply: (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode?) = (CGSize(), { _ in nil })
if displayHeader {
let bubbleWidthInsets: CGFloat = mosaicRange == nil ? layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right : 0.0
if authorNameString != nil || inlineBotNameString != nil {
@ -2824,9 +2847,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
var detachedContentNodesHeight: CGFloat = 0.0
var mosaicStatusOrigin: CGPoint?
var unlockButtonPosition: CGPoint?
var mediaInfoOrigin: CGPoint?
for i in 0 ..< contentNodePropertiesAndFinalize.count {
let (properties, position, finalize, contentGroupId, itemSelection) = contentNodePropertiesAndFinalize[i]
if let position = position, case let .linear(top, bottom) = position {
if case let .Neighbour(_, _, spacing) = top, case let .overlap(overlap) = spacing {
currentContainerGroupOverlap = overlap
@ -2851,12 +2876,17 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
contentNodeFramesPropertiesAndApply.append((contentNodeFrame, properties, true, apply))
if i == mosaicRange.upperBound - 1 {
unlockButtonPosition = CGPoint(x: size.width / 2.0, y: contentNodesHeight + size.height / 2.0)
mediaInfoOrigin = CGPoint(x: size.width, y: contentNodesHeight)
contentNodesHeight += size.height
totalContentNodesHeight += size.height
mosaicStatusOrigin = contentNodeFrame.bottomRight
}
} else {
let contentProperties = contentPropertiesAndLayouts[i].3
if i == 0 && !headerSize.height.isZero {
if contentGroupId == nil {
contentNodesHeight += properties.headerSpacing
@ -2870,7 +2900,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if !contentContainerNodeFrames.isEmpty {
overlapOffset = currentContainerGroupOverlap
}
contentContainerNodeFrames.append((containerGroupId, CGRect(x: 0.0, y: headerSize.height + totalContentNodesHeight - contentNodesHeight - overlapOffset, width: maxContentWidth, height: contentNodesHeight), currentItemSelection, currentContainerGroupOverlap))
let containerFrame = CGRect(x: 0.0, y: headerSize.height + totalContentNodesHeight - contentNodesHeight - overlapOffset, width: maxContentWidth, height: contentNodesHeight)
contentContainerNodeFrames.append((containerGroupId, containerFrame, currentItemSelection, currentContainerGroupOverlap))
if !overlapOffset.isZero {
totalContentNodesHeight -= currentContainerGroupOverlap
}
@ -2885,7 +2917,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
let contentNodeOriginY = contentNodesHeight - detachedContentNodesHeight
let (size, apply) = finalize(maxContentWidth)
contentNodeFramesPropertiesAndApply.append((CGRect(origin: CGPoint(x: 0.0, y: contentNodeOriginY), size: size), properties, contentGroupId == nil, apply))
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentNodeOriginY), size: size)
contentNodeFramesPropertiesAndApply.append((containerFrame, properties, contentGroupId == nil, apply))
if contentProperties.neighborType == .media && unlockButtonPosition == nil {
unlockButtonPosition = containerFrame.center
mediaInfoOrigin = CGPoint(x: containerFrame.width, y: containerFrame.minY)
}
contentNodesHeight += size.height
totalContentNodesHeight += size.height
@ -2909,6 +2947,34 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
contentSize.height += totalContentNodesHeight
if let paidContent = item.message.media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent, let media = paidContent.extendedMedia.first {
if case .preview = media {
let sizeAndApply = unlockButtonLayout(ChatMessageUnlockMediaNode.Arguments(
presentationData: item.presentationData,
strings: item.presentationData.strings,
context: item.context,
controllerInteraction: item.controllerInteraction,
message: item.message,
media: paidContent,
constrainedSize: CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude),
animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
))
unlockButtonSizeApply = (sizeAndApply.0, { synchronousLoads in sizeAndApply.1(synchronousLoads) })
} else {
let sizeAndApply = mediaInfoLayout(ChatMessageStarsMediaInfoNode.Arguments(
presentationData: item.presentationData,
context: item.context,
message: item.message,
media: paidContent,
constrainedSize: CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude),
animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
))
mediaInfoSizeApply = (sizeAndApply.0, { synchronousLoads in sizeAndApply.1(synchronousLoads) })
}
}
var actionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)?
if let actionButtonsFinalize = actionButtonsFinalize {
actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth)
@ -3039,6 +3105,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
contentContainerNodeFrames: contentContainerNodeFrames,
mosaicStatusOrigin: mosaicStatusOrigin,
mosaicStatusSizeAndApply: mosaicStatusSizeAndApply,
unlockButtonPosition: unlockButtonPosition,
unlockButtonSizeAndApply: unlockButtonSizeApply,
mediaInfoOrigin: mediaInfoOrigin,
mediaInfoSizeAndApply: mediaInfoSizeApply,
needsShareButton: needsShareButton,
shareButtonOffset: shareButtonOffset,
avatarOffset: avatarOffset,
@ -3093,6 +3163,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
contentContainerNodeFrames: [(UInt32, CGRect, Bool?, CGFloat)],
mosaicStatusOrigin: CGPoint?,
mosaicStatusSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)?,
unlockButtonPosition: CGPoint?,
unlockButtonSizeAndApply: (CGSize, (Bool) -> ChatMessageUnlockMediaNode?),
mediaInfoOrigin: CGPoint?,
mediaInfoSizeAndApply: (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode?),
needsShareButton: Bool,
shareButtonOffset: CGPoint?,
avatarOffset: CGFloat?,
@ -4096,6 +4170,56 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
strongSelf.mosaicStatusNode = nil
mosaicStatusNode.removeFromSupernode()
}
if let unlockButtonPosition {
let (size, apply) = unlockButtonSizeAndApply
var unlockButtonNodeAnimation = animation
if strongSelf.unlockButtonNode == nil {
unlockButtonNodeAnimation = .None
}
let unlockButtonNode = apply(strongSelf.unlockButtonNode != nil)
if unlockButtonNode !== strongSelf.unlockButtonNode {
strongSelf.unlockButtonNode?.removeFromSupernode()
strongSelf.unlockButtonNode = unlockButtonNode
if let unlockButtonNode {
strongSelf.clippingNode.addSubnode(unlockButtonNode)
}
}
let absoluteOrigin = unlockButtonPosition.offsetBy(dx: contentOrigin.x, dy: contentOrigin.y)
if let unlockButtonNode {
unlockButtonNodeAnimation.animator.updateFrame(layer: unlockButtonNode.layer, frame: CGRect(origin: CGPoint(x: floor(absoluteOrigin.x - size.width / 2.0), y: floor(absoluteOrigin.y - size.height / 2.0)), size: size), completion: nil)
}
} else if let unlockButtonNode = strongSelf.unlockButtonNode {
strongSelf.unlockButtonNode = nil
unlockButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
unlockButtonNode.removeFromSupernode()
})
}
if let mediaInfoOrigin {
let (size, apply) = mediaInfoSizeAndApply
var unlockButtonNodeAnimation = animation
if strongSelf.unlockButtonNode == nil {
unlockButtonNodeAnimation = .None
}
let mediaInfoNode = apply(strongSelf.mediaInfoNode != nil)
if mediaInfoNode !== strongSelf.mediaInfoNode {
strongSelf.mediaInfoNode?.removeFromSupernode()
strongSelf.mediaInfoNode = mediaInfoNode
if let mediaInfoNode {
strongSelf.clippingNode.addSubnode(mediaInfoNode)
}
}
let absoluteOrigin = mediaInfoOrigin.offsetBy(dx: contentOrigin.x, dy: contentOrigin.y)
if let mediaInfoNode {
unlockButtonNodeAnimation.animator.updateFrame(layer: mediaInfoNode.layer, frame: CGRect(origin: CGPoint(x: absoluteOrigin.x - size.width - 8.0, y: absoluteOrigin.y + 8.0), size: size), completion: nil)
}
} else if let mediaInfoNode = strongSelf.mediaInfoNode {
strongSelf.mediaInfoNode = nil
mediaInfoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
mediaInfoNode.removeFromSupernode()
})
}
if needsShareButton {
if strongSelf.shareButtonNode == nil {
@ -5157,6 +5281,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
override public func updateHiddenMedia() {
var hasHiddenMediaInfo = false
var hasHiddenMosaicStatus = false
var hasHiddenBackground = false
if let item = self.item {
@ -5169,6 +5294,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if let mosaicStatusNode = self.mosaicStatusNode, mosaicStatusNode.frame.intersects(contentNode.frame) {
hasHiddenMosaicStatus = true
}
if let mediaInfoNode = self.mediaInfoNode, mediaInfoNode.frame.intersects(contentNode.frame) {
hasHiddenMediaInfo = true
}
}
}
}
@ -5185,6 +5313,17 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
}
if let mediaInfoNode = self.mediaInfoNode {
if mediaInfoNode.alpha.isZero != hasHiddenMediaInfo {
if hasHiddenMediaInfo {
mediaInfoNode.alpha = 0.0
} else {
mediaInfoNode.alpha = 1.0
mediaInfoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}
self.backgroundNode.isHidden = hasHiddenBackground
self.backgroundWallpaperNode.isHidden = hasHiddenBackground
}

View file

@ -2386,7 +2386,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
var viewText: String = ""
if message.isAgeRestricted() {
//TODO: localize
//TODO:localize
viewText = "18+ Content"
} else {
outer: for attribute in message.attributes {
@ -2403,9 +2403,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}
}
}
if let paidContent {
viewText = "⭐️\(paidContent.amount)"
}
// if let paidContent {
// viewText = "\(paidContent.amount)"
// }
self.extendedMediaOverlayNode?.update(size: self.imageNode.frame.size, text: viewText, imageSignal: self.currentBlurredImageSignal, imageFrame: self.imageNode.view.convert(self.imageNode.bounds, to: self.extendedMediaOverlayNode?.view), corners: self.currentImageArguments?.corners)
} else if let extendedMediaOverlayNode = self.extendedMediaOverlayNode {
self.extendedMediaOverlayNode = nil

View file

@ -56,7 +56,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
case .automaticPlayback:
openChatMessageMode = .automaticPlayback
}
let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: openChatMessageMode))
let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: openChatMessageMode, mediaIndex: self.mediaIndex))
}
self.interactiveImageNode.activateAgeRestrictedMedia = { [weak self] in

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,288 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import LocalizedPeerData
import PhotoResources
import TelegramStringFormatting
import TextFormat
import TextNodeWithEntities
import AnimationCache
import MultiAnimationRenderer
import ComponentFlow
import ChatControllerInteraction
private func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat) -> (CGPoint, UIImage?) {
enum CornerType {
case topLeft
case topRight
case bottomLeft
case bottomRight
}
func drawFullCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) {
if radius.isZero {
return
}
context.setFillColor(color.cgColor)
switch type {
case .topLeft:
context.clear(CGRect(origin: point, size: CGSize(width: radius, height: radius)))
context.fillEllipse(in: CGRect(origin: point, size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .topRight:
context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .bottomLeft:
context.clear(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .bottomRight:
context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
}
}
func drawConnectingCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) {
context.setFillColor(color.cgColor)
switch type {
case .topLeft:
context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius)))
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .topRight:
context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius, height: radius)))
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .bottomLeft:
context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius)))
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .bottomRight:
context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius)))
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
}
}
if rects.isEmpty {
return (CGPoint(), nil)
}
var topLeft = rects[0].origin
var bottomRight = CGPoint(x: rects[0].maxX, y: rects[0].maxY)
for i in 1 ..< rects.count {
topLeft.x = min(topLeft.x, rects[i].origin.x)
topLeft.y = min(topLeft.y, rects[i].origin.y)
bottomRight.x = max(bottomRight.x, rects[i].maxX)
bottomRight.y = max(bottomRight.y, rects[i].maxY)
}
topLeft.x -= inset
topLeft.y -= inset
bottomRight.x += inset * 2.0
bottomRight.y += inset * 2.0
return (topLeft, generateImage(CGSize(width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.setBlendMode(.copy)
for i in 0 ..< rects.count {
let rect = rects[i].insetBy(dx: -inset, dy: -inset)
context.fill(rect.offsetBy(dx: -topLeft.x, dy: -topLeft.y))
}
for i in 0 ..< rects.count {
let rect = rects[i].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
var previous: CGRect?
if i != 0 {
previous = rects[i - 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
}
var next: CGRect?
if i != rects.count - 1 {
next = rects[i + 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
}
if let previous = previous {
if previous.contains(rect.topLeft) {
if abs(rect.topLeft.x - previous.minX) >= innerRadius {
var radius = innerRadius
if let next = next {
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
}
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topLeft.x, y: previous.maxY), type: .topLeft, radius: radius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius)
}
if previous.contains(rect.topRight.offsetBy(dx: -1.0, dy: 0.0)) {
if abs(rect.topRight.x - previous.maxX) >= innerRadius {
var radius = innerRadius
if let next = next {
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
}
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topRight.x, y: previous.maxY), type: .topRight, radius: radius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius)
drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius)
}
if let next = next {
if next.contains(rect.bottomLeft) {
if abs(rect.bottomRight.x - next.maxX) >= innerRadius {
var radius = innerRadius
if let previous = previous {
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
}
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomLeft.x, y: next.minY), type: .bottomLeft, radius: radius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius)
}
if next.contains(rect.bottomRight.offsetBy(dx: -1.0, dy: 0.0)) {
if abs(rect.bottomRight.x - next.maxX) >= innerRadius {
var radius = innerRadius
if let previous = previous {
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
}
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomRight.x, y: next.minY), type: .bottomRight, radius: radius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius)
drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius)
}
}
}))
}
public enum ChatMessageThreadInfoType {
case bubble(incoming: Bool)
case standalone
}
public class ChatMessageStarsMediaInfoNode: ASDisplayNode {
public class Arguments {
public let presentationData: ChatPresentationData
public let context: AccountContext
public let message: Message
public let media: TelegramMediaPaidContent
public let constrainedSize: CGSize
public let animationCache: AnimationCache?
public let animationRenderer: MultiAnimationRenderer?
public init(
presentationData: ChatPresentationData,
context: AccountContext,
message: Message,
media: TelegramMediaPaidContent,
constrainedSize: CGSize,
animationCache: AnimationCache?,
animationRenderer: MultiAnimationRenderer?
) {
self.presentationData = presentationData
self.context = context
self.message = message
self.media = media
self.constrainedSize = constrainedSize
self.animationCache = animationCache
self.animationRenderer = animationRenderer
}
}
public var visibility: Bool = false {
didSet {
if self.visibility != oldValue {
self.textNode?.visibilityRect = self.visibility ? CGRect.infinite : nil
}
}
}
private let contentBackgroundNode: ASImageNode
private var textNode: TextNodeWithEntities?
override public init() {
self.contentBackgroundNode = ASImageNode()
self.contentBackgroundNode.displaysAsynchronously = false
self.contentBackgroundNode.displayWithoutProcessing = true
self.contentBackgroundNode.isLayerBacked = true
self.contentBackgroundNode.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.contentBackgroundNode)
}
public class func asyncLayout(_ maybeNode: ChatMessageStarsMediaInfoNode?) -> (_ arguments: Arguments) -> (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode) {
let textNodeLayout = TextNodeWithEntities.asyncLayout(maybeNode?.textNode)
return { arguments in
let fontSize = floor(arguments.presentationData.fontSize.baseDisplaySize * 11.0 / 17.0)
let textFont = Font.regular(fontSize)
let text: NSMutableAttributedString
if let peer = arguments.message.peers[arguments.message.id.peerId] as? TelegramChannel, peer.flags.contains(.isCreator) || peer.adminRights != nil {
text = NSMutableAttributedString(string: "⭐️\(arguments.media.amount)", font: textFont, textColor: .white)
} else {
text = NSMutableAttributedString(string: "Purchased", font: textFont, textColor: .white)
}
if let range = text.string.range(of: "⭐️") {
text.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: NSRange(range, in: text.string))
text.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: text.string))
}
let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: arguments.constrainedSize.width, height: arguments.constrainedSize.height), alignment: .natural, cutout: nil, insets: .zero))
let padding: CGFloat = 6.0
let size = CGSize(width: textLayout.size.width + padding * 2.0, height: 18.0)
return (size, { attemptSynchronous in
let node: ChatMessageStarsMediaInfoNode
if let maybeNode = maybeNode {
node = maybeNode
} else {
node = ChatMessageStarsMediaInfoNode()
}
if node.contentBackgroundNode.image == nil {
node.contentBackgroundNode.image = generateStretchableFilledCircleImage(radius: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.3))
}
node.textNode?.textNode.displaysAsynchronously = !arguments.presentationData.isPreview
var textArguments: TextNodeWithEntities.Arguments?
if let cache = arguments.animationCache, let renderer = arguments.animationRenderer {
textArguments = TextNodeWithEntities.Arguments(context: arguments.context, cache: cache, renderer: renderer, placeholderColor: .clear, attemptSynchronous: attemptSynchronous)
}
let textNode = textApply(textArguments)
textNode.visibilityRect = node.visibility ? CGRect.infinite : nil
if node.textNode == nil {
textNode.textNode.isUserInteractionEnabled = false
node.textNode = textNode
node.addSubnode(textNode.textNode)
}
node.contentBackgroundNode.frame = CGRect(origin: .zero, size: size)
let textFrame = CGRect(origin: CGPoint(x: padding, y: floorToScreenPixels((size.height - textLayout.size.height) / 2.0) + UIScreenPixel), size: textLayout.size)
textNode.textNode.frame = textFrame
return node
})
}
}
}

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,165 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import LocalizedPeerData
import PhotoResources
import TelegramStringFormatting
import TextFormat
import TextNodeWithEntities
import AnimationCache
import MultiAnimationRenderer
import ComponentFlow
import ChatControllerInteraction
public class ChatMessageUnlockMediaNode: ASDisplayNode {
public class Arguments {
public let presentationData: ChatPresentationData
public let strings: PresentationStrings
public let context: AccountContext
public let controllerInteraction: ChatControllerInteraction
public let message: Message
public let media: TelegramMediaPaidContent
public let constrainedSize: CGSize
public let animationCache: AnimationCache?
public let animationRenderer: MultiAnimationRenderer?
public init(
presentationData: ChatPresentationData,
strings: PresentationStrings,
context: AccountContext,
controllerInteraction: ChatControllerInteraction,
message: Message,
media: TelegramMediaPaidContent,
constrainedSize: CGSize,
animationCache: AnimationCache?,
animationRenderer: MultiAnimationRenderer?
) {
self.presentationData = presentationData
self.strings = strings
self.context = context
self.controllerInteraction = controllerInteraction
self.message = message
self.media = media
self.constrainedSize = constrainedSize
self.animationCache = animationCache
self.animationRenderer = animationRenderer
}
}
public var visibility: Bool = false {
didSet {
if self.visibility != oldValue {
self.textNode?.visibilityRect = self.visibility ? CGRect.infinite : nil
}
}
}
private let contentNode: HighlightTrackingButtonNode
private let backgroundNode: NavigationBackgroundNode
private var textNode: TextNodeWithEntities?
private var pressed = { }
private var absolutePosition: (CGRect, CGSize)?
override public init() {
self.contentNode = HighlightTrackingButtonNode()
self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x000000, alpha: 0.3))
super.init()
self.contentNode.isUserInteractionEnabled = true
self.addSubnode(self.contentNode)
self.contentNode.addSubnode(self.backgroundNode)
// self.contentNode.highligthedChanged = { [weak self] highlighted in
// if let strongSelf = self {
// if highlighted, !strongSelf.frame.width.isZero {
// let scale = (strongSelf.frame.width - 10.0) / strongSelf.frame.width
//
// strongSelf.contentNode.layer.animateScale(from: 1.0, to: scale, duration: 0.15, removeOnCompletion: false)
//
// strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity")
// strongSelf.backgroundNode.alpha = 0.2
// } else if let presentationLayer = strongSelf.contentNode.layer.presentation() {
// strongSelf.contentNode.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false)
//
// strongSelf.backgroundNode.alpha = 1.0
// strongSelf.backgroundNode.layer.animateAlpha(from: 0.2, to: 1.0, duration: 0.2)
// }
// }
// }
self.contentNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
@objc private func buttonPressed() {
self.pressed()
}
public class func asyncLayout(_ maybeNode: ChatMessageUnlockMediaNode?) -> (_ arguments: Arguments) -> (CGSize, (Bool) -> ChatMessageUnlockMediaNode) {
let textNodeLayout = TextNodeWithEntities.asyncLayout(maybeNode?.textNode)
return { arguments in
let fontSize = floor(arguments.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)
let textFont = Font.medium(fontSize)
let padding: CGFloat = 10.0
//TODO:localize
let text = NSMutableAttributedString(string: "Unlock for ⭐️ \(arguments.media.amount)", font: textFont, textColor: .white)
if let range = text.string.range(of: "⭐️") {
text.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: text.string))
text.addAttribute(.baselineOffset, value: 0.5, range: NSRange(range, in: text.string))
}
let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: arguments.constrainedSize.width, height: arguments.constrainedSize.height), alignment: .natural, cutout: nil, insets: .zero))
let size = CGSize(width: textLayout.size.width + padding * 2.0, height: 32.0)
return (size, { attemptSynchronous in
let node: ChatMessageUnlockMediaNode
if let maybeNode = maybeNode {
node = maybeNode
} else {
node = ChatMessageUnlockMediaNode()
}
node.pressed = {
let _ = arguments.controllerInteraction.openMessage(arguments.message, OpenMessageParams(mode: .default))
}
node.textNode?.textNode.displaysAsynchronously = !arguments.presentationData.isPreview
var textArguments: TextNodeWithEntities.Arguments?
if let cache = arguments.animationCache, let renderer = arguments.animationRenderer {
textArguments = TextNodeWithEntities.Arguments(context: arguments.context, cache: cache, renderer: renderer, placeholderColor: .clear, attemptSynchronous: attemptSynchronous)
}
let textNode = textApply(textArguments)
textNode.visibilityRect = node.visibility ? CGRect.infinite : nil
if node.textNode == nil {
textNode.textNode.isUserInteractionEnabled = false
node.textNode = textNode
node.contentNode.addSubnode(textNode.textNode)
}
let textFrame = CGRect(origin: CGPoint(x: padding, y: floorToScreenPixels((size.height - textLayout.size.height) / 2.0)), size: textLayout.size)
textNode.textNode.frame = textFrame
node.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
node.backgroundNode.update(size: size, cornerRadius: size.height / 2.0, transition: .immediate)
node.contentNode.frame = CGRect(origin: CGPoint(), size: size)
return node
})
}
}
}

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

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

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

@ -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,472 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import ComponentFlow
import TelegramPresentationData
import PhotoResources
import AvatarNode
import AccountContext
import BundleIconComponent
import MultilineTextComponent
public final class StarsAvatarComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let peer: StarsContext.State.Transaction.Peer
let photo: TelegramMediaWebFile?
let media: [Media]
public init(context: AccountContext, theme: PresentationTheme, peer: StarsContext.State.Transaction.Peer, photo: TelegramMediaWebFile?, media: [Media]) {
self.context = context
self.theme = theme
self.peer = peer
self.photo = photo
self.media = media
}
public static func ==(lhs: StarsAvatarComponent, rhs: StarsAvatarComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.photo != rhs.photo {
return false
}
if !areMediaArraysEqual(lhs.media, rhs.media) {
return false
}
return true
}
public final class View: UIView {
private let avatarNode: AvatarNode
private let backgroundView = UIImageView()
private let iconView = UIImageView()
private var imageNode: TransformImageNode?
private let fetchDisposable = MetaDisposable()
private var component: StarsAvatarComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0))
super.init(frame: frame)
self.iconView.contentMode = .scaleAspectFit
self.addSubnode(self.avatarNode)
self.addSubview(self.backgroundView)
self.addSubview(self.iconView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.fetchDisposable.dispose()
}
func update(component: StarsAvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let size = CGSize(width: 40.0, height: 40.0)
var iconInset: CGFloat = 3.0
var iconOffset: CGFloat = 0.0
var dimensions = size
switch component.peer {
case let .peer(peer):
if !component.media.isEmpty {
let imageNode: TransformImageNode
if let current = self.imageNode {
imageNode = current
} else {
imageNode = TransformImageNode()
imageNode.contentAnimations = [.subsequentUpdates]
self.addSubview(imageNode.view)
self.imageNode = imageNode
if let image = component.media.first as? TelegramMediaImage {
if let imageDimensions = largestImageRepresentation(image.representations)?.dimensions {
dimensions = imageDimensions.cgSize.aspectFilled(size)
}
imageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false))
self.fetchDisposable.set(chatMessagePhotoInteractiveFetched(context: component.context, userLocation: .other, photoReference: .standalone(media: image), displayAtSize: nil, storeToDownloadsPeerId: nil).startStrict())
} else if let file = component.media.first as? TelegramMediaFile {
if let videoDimensions = file.dimensions {
dimensions = videoDimensions.cgSize.aspectFilled(size)
}
imageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file), autoFetchFullSizeThumbnail: true))
}
}
imageNode.frame = CGRect(origin: .zero, size: size)
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 8.0), imageSize: dimensions, boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
self.backgroundView.isHidden = true
self.iconView.isHidden = true
self.avatarNode.isHidden = true
} else if let photo = component.photo {
let imageNode: TransformImageNode
if let current = self.imageNode {
imageNode = current
} else {
imageNode = TransformImageNode()
imageNode.contentAnimations = [.subsequentUpdates]
self.addSubview(imageNode.view)
self.imageNode = imageNode
imageNode.setSignal(chatWebFileImage(account: component.context.account, file: photo))
self.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: component.context.account, userLocation: .other, image: photo).startStrict())
}
imageNode.frame = CGRect(origin: .zero, size: size)
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 8.0), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
self.backgroundView.isHidden = true
self.iconView.isHidden = true
self.avatarNode.isHidden = true
} else {
self.avatarNode.setPeer(
context: component.context,
theme: component.theme,
peer: peer,
synchronousLoad: true
)
self.backgroundView.isHidden = true
self.iconView.isHidden = true
self.avatarNode.isHidden = false
}
case .appStore:
self.backgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0x2a9ef1).cgColor,
UIColor(rgb: 0x72d5fd).cgColor
],
direction: .mirroredDiagonal
)
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Apple")
case .playMarket:
self.backgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0x54cb68).cgColor,
UIColor(rgb: 0xa0de7e).cgColor
],
direction: .mirroredDiagonal
)
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Google")
case .fragment:
self.backgroundView.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1b1f24))
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment")
iconOffset = 2.0
case .premiumBot:
iconInset = 7.0
self.backgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0x6b93ff).cgColor,
UIColor(rgb: 0x6b93ff).cgColor,
UIColor(rgb: 0x8d77ff).cgColor,
UIColor(rgb: 0xb56eec).cgColor,
UIColor(rgb: 0xb56eec).cgColor
],
direction: .mirroredDiagonal
)
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white)
case .unsupported:
iconInset = 7.0
self.backgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0xb1b1b1).cgColor,
UIColor(rgb: 0xcdcdcd).cgColor
],
direction: .mirroredDiagonal
)
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white)
}
self.avatarNode.frame = CGRect(origin: .zero, size: size)
self.iconView.frame = CGRect(origin: .zero, size: size).insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset)
self.backgroundView.frame = CGRect(origin: .zero, size: size)
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class StarsLabelComponent: CombinedComponent {
let text: NSAttributedString
public init(
text: NSAttributedString
) {
self.text = text
}
public static func ==(lhs: StarsLabelComponent, rhs: StarsLabelComponent) -> Bool {
if lhs.text != rhs.text {
return false
}
return true
}
public static var body: Body {
let text = Child(MultilineTextComponent.self)
let icon = Child(BundleIconComponent.self)
return { context in
let component = context.component
let text = text.update(
component: MultilineTextComponent(text: .plain(component.text)),
availableSize: CGSize(width: 100.0, height: 40.0),
transition: context.transition
)
let iconSize = CGSize(width: 20.0, height: 20.0)
let icon = icon.update(
component: BundleIconComponent(
name: "Premium/Stars/StarLarge",
tintColor: nil
),
availableSize: iconSize,
transition: context.transition
)
let spacing: CGFloat = 3.0
let totalWidth = text.size.width + spacing + iconSize.width
let size = CGSize(width: totalWidth, height: iconSize.height)
context.add(text
.position(CGPoint(x: text.size.width / 2.0, y: size.height / 2.0))
)
context.add(icon
.position(CGPoint(x: totalWidth - iconSize.width / 2.0, y: size.height / 2.0 - UIScreenPixel))
)
return size
}
}
}
public final class StarsMediaComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let peer: StarsContext.State.Transaction.Peer
let photo: TelegramMediaWebFile?
public init(context: AccountContext, theme: PresentationTheme, peer: StarsContext.State.Transaction.Peer, photo: TelegramMediaWebFile?) {
self.context = context
self.theme = theme
self.peer = peer
self.photo = photo
}
public static func ==(lhs: StarsMediaComponent, rhs: StarsMediaComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.photo != rhs.photo {
return false
}
return true
}
public final class View: UIView {
private let avatarNode: AvatarNode
private let backgroundView = UIImageView()
private let iconView = UIImageView()
private var imageNode: TransformImageNode?
private let fetchDisposable = MetaDisposable()
private var component: StarsMediaComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0))
super.init(frame: frame)
self.iconView.contentMode = .scaleAspectFit
self.addSubnode(self.avatarNode)
self.addSubview(self.backgroundView)
self.addSubview(self.iconView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.fetchDisposable.dispose()
}
func update(component: StarsMediaComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let size = CGSize(width: 40.0, height: 40.0)
var iconInset: CGFloat = 3.0
var iconOffset: CGFloat = 0.0
switch component.peer {
case let .peer(peer):
if let photo = component.photo {
let imageNode: TransformImageNode
if let current = self.imageNode {
imageNode = current
} else {
imageNode = TransformImageNode()
imageNode.contentAnimations = [.subsequentUpdates]
self.addSubview(imageNode.view)
self.imageNode = imageNode
imageNode.setSignal(chatWebFileImage(account: component.context.account, file: photo))
self.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: component.context.account, userLocation: .other, image: photo).startStrict())
}
imageNode.frame = CGRect(origin: .zero, size: size)
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: size.width / 2.0), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
self.backgroundView.isHidden = true
self.iconView.isHidden = true
self.avatarNode.isHidden = true
} else {
self.avatarNode.setPeer(
context: component.context,
theme: component.theme,
peer: peer,
synchronousLoad: true
)
self.backgroundView.isHidden = true
self.iconView.isHidden = true
self.avatarNode.isHidden = false
}
case .appStore:
self.backgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0x2a9ef1).cgColor,
UIColor(rgb: 0x72d5fd).cgColor
],
direction: .mirroredDiagonal
)
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Apple")
case .playMarket:
self.backgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0x54cb68).cgColor,
UIColor(rgb: 0xa0de7e).cgColor
],
direction: .mirroredDiagonal
)
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Google")
case .fragment:
self.backgroundView.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1b1f24))
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment")
iconOffset = 2.0
case .premiumBot:
iconInset = 7.0
self.backgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0x6b93ff).cgColor,
UIColor(rgb: 0x6b93ff).cgColor,
UIColor(rgb: 0x8d77ff).cgColor,
UIColor(rgb: 0xb56eec).cgColor,
UIColor(rgb: 0xb56eec).cgColor
],
direction: .mirroredDiagonal
)
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white)
case .unsupported:
iconInset = 7.0
self.backgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0xb1b1b1).cgColor,
UIColor(rgb: 0xcdcdcd).cgColor
],
direction: .mirroredDiagonal
)
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white)
}
self.avatarNode.frame = CGRect(origin: .zero, size: size)
self.iconView.frame = CGRect(origin: .zero, size: size).insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset)
self.backgroundView.frame = CGRect(origin: .zero, size: size)
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View file

@ -247,25 +247,64 @@ public final class StarsImageComponent: Component {
public enum Subject: Equatable {
case none
case photo(TelegramMediaWebFile)
case extendedMedia(TelegramExtendedMedia)
case media([Media])
case extendedMedia([TelegramExtendedMedia])
case transactionPeer(StarsContext.State.Transaction.Peer)
public static func == (lhs: StarsImageComponent.Subject, rhs: StarsImageComponent.Subject) -> Bool {
switch lhs {
case .none:
if case .none = rhs {
return true
} else {
return false
}
case let .photo(lhsPhoto):
if case let .photo(rhsPhoto) = rhs, lhsPhoto == rhsPhoto {
return true
} else {
return false
}
case let .media(lhsMedia):
if case let .media(rhsMedia) = rhs, areMediaArraysEqual(lhsMedia, rhsMedia) {
return true
} else {
return false
}
case let .extendedMedia(lhsExtendedMedia):
if case let .extendedMedia(rhsExtendedMedia) = rhs, lhsExtendedMedia == rhsExtendedMedia {
return true
} else {
return false
}
case let .transactionPeer(lhsPeer):
if case let .transactionPeer(rhsPeer) = rhs, lhsPeer == rhsPeer {
return true
} else {
return false
}
}
}
}
public let context: AccountContext
public let subject: Subject
public let theme: PresentationTheme
public let diameter: CGFloat
public let backgroundColor: UIColor
public init(
context: AccountContext,
subject: Subject,
theme: PresentationTheme,
diameter: CGFloat
diameter: CGFloat,
backgroundColor: UIColor
) {
self.context = context
self.subject = subject
self.theme = theme
self.diameter = diameter
self.backgroundColor = backgroundColor
}
public static func ==(lhs: StarsImageComponent, rhs: StarsImageComponent) -> Bool {
@ -275,9 +314,15 @@ public final class StarsImageComponent: Component {
if lhs.subject != rhs.subject {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.diameter != rhs.diameter {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
return true
}
@ -288,11 +333,15 @@ public final class StarsImageComponent: Component {
private var largeParticlesView: StarsParticlesView?
private var imageNode: TransformImageNode?
private var imageFrameNode: UIView?
private var secondImageNode: TransformImageNode?
private var avatarNode: ImageNode?
private var iconBackgroundView: UIImageView?
private var iconView: UIImageView?
private var dustNode: MediaDustNode?
private var countView = ComponentView<Empty>()
private let fetchDisposable = MetaDisposable()
public override init(frame: CGRect) {
@ -355,6 +404,77 @@ public final class StarsImageComponent: Component {
imageNode.frame = imageFrame
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: imageSize.width / 2.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
case let .media(media):
let imageNode: TransformImageNode
var dimensions = imageSize
if let current = self.imageNode {
imageNode = current
} else {
imageNode = TransformImageNode()
imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
self.addSubview(imageNode.view)
self.imageNode = imageNode
if let image = media.first as? TelegramMediaImage {
if let imageDimensions = largestImageRepresentation(image.representations)?.dimensions {
dimensions = imageDimensions.cgSize.aspectFilled(imageSize)
}
imageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false))
} else if let file = media.first as? TelegramMediaFile {
if let videoDimensions = file.dimensions {
dimensions = videoDimensions.cgSize.aspectFilled(imageSize)
}
imageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file), useLargeThumbnail: true, autoFetchFullSizeThumbnail: true))
}
}
imageNode.frame = imageFrame
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: dimensions, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
if media.count > 1 {
let secondImageNode: TransformImageNode
let imageFrameNode: UIView
if let current = self.secondImageNode, let currentFrame = self.imageFrameNode {
secondImageNode = current
imageFrameNode = currentFrame
} else {
secondImageNode = TransformImageNode()
secondImageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
self.insertSubview(secondImageNode.view, belowSubview: imageNode.view)
self.secondImageNode = secondImageNode
imageFrameNode = UIView()
imageFrameNode.layer.cornerRadius = 17.0
self.insertSubview(imageFrameNode, belowSubview: imageNode.view)
self.imageFrameNode = imageFrameNode
if let image = media[1] as? TelegramMediaImage {
if let imageDimensions = largestImageRepresentation(image.representations)?.dimensions {
dimensions = imageDimensions.cgSize.aspectFilled(imageSize)
}
secondImageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false))
}
}
imageFrameNode.backgroundColor = component.backgroundColor
secondImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
secondImageNode.frame = imageFrame.offsetBy(dx: 6.0, dy: -6.0)
imageFrameNode.frame = imageFrame.insetBy(dx: -2.0, dy: -2.0)
let countSize = self.countView.update(
transition: .immediate,
component: AnyComponent(
Text(text: "\(media.count)", font: Font.with(size: 30.0, design: .round, weight: .medium), color: .white)
),
environment: {},
containerSize: imageFrame.size
)
let countFrame = CGRect(origin: CGPoint(x: imageFrame.minX + floorToScreenPixels((imageFrame.width - countSize.width) / 2.0), y: imageFrame.minY + floorToScreenPixels((imageFrame.height - countSize.height) / 2.0)), size: countSize)
if let countView = self.countView.view {
if countView.superview == nil {
self.addSubview(countView)
}
countView.frame = countFrame
}
}
case let .extendedMedia(extendedMedia):
let imageNode: TransformImageNode
let dustNode: MediaDustNode
@ -368,12 +488,12 @@ public final class StarsImageComponent: Component {
self.imageNode = imageNode
let media: TelegramMediaImage
switch extendedMedia {
switch extendedMedia.first {
case let .preview(_, immediateThumbnailData, _):
let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
media = thumbnailMedia
case let .full(fullMedia):
media = fullMedia as! TelegramMediaImage
default:
fatalError()
}
imageNode.setSignal(chatSecretPhoto(account: component.context.account, userLocation: .other, photoReference: .standalone(media: media), ignoreFullSize: true, synchronousLoad: true))
@ -383,11 +503,63 @@ public final class StarsImageComponent: Component {
self.dustNode = dustNode
}
if extendedMedia.count > 1 {
let secondImageNode: TransformImageNode
let imageFrameNode: UIView
if let current = self.secondImageNode, let currentFrame = self.imageFrameNode {
secondImageNode = current
imageFrameNode = currentFrame
} else {
secondImageNode = TransformImageNode()
secondImageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
self.insertSubview(secondImageNode.view, belowSubview: imageNode.view)
self.secondImageNode = secondImageNode
imageFrameNode = UIView()
imageFrameNode.layer.cornerRadius = 17.0
self.insertSubview(imageFrameNode, belowSubview: imageNode.view)
self.imageFrameNode = imageFrameNode
let media: TelegramMediaImage
switch extendedMedia[1] {
case let .preview(_, immediateThumbnailData, _):
let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
media = thumbnailMedia
default:
fatalError()
}
secondImageNode.setSignal(chatSecretPhoto(account: component.context.account, userLocation: .other, photoReference: .standalone(media: media), ignoreFullSize: true, synchronousLoad: true))
}
imageFrameNode.backgroundColor = component.backgroundColor
secondImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
secondImageNode.frame = imageFrame.offsetBy(dx: 6.0, dy: -6.0)
imageFrameNode.frame = imageFrame.insetBy(dx: -2.0, dy: -2.0)
}
imageNode.frame = imageFrame
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 12.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
dustNode.frame = imageFrame
dustNode.update(size: imageFrame.size, color: .white, transition: .immediate)
if extendedMedia.count > 1 {
let countSize = self.countView.update(
transition: .immediate,
component: AnyComponent(
Text(text: "\(extendedMedia.count)", font: Font.with(size: 30.0, design: .round, weight: .medium), color: .white)
),
environment: {},
containerSize: imageFrame.size
)
let countFrame = CGRect(origin: CGPoint(x: imageFrame.minX + floorToScreenPixels((imageFrame.width - countSize.width) / 2.0), y: imageFrame.minY + floorToScreenPixels((imageFrame.height - countSize.height) / 2.0)), size: countSize)
if let countView = self.countView.view {
if countView.superview == nil {
self.addSubview(countView)
}
countView.frame = countFrame
}
}
case let .transactionPeer(peer):
if case let .peer(peer) = peer {
let avatarNode: ImageNode

View file

@ -0,0 +1,39 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "StarsTransactionScreen",
module_name = "StarsTransactionScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/TelegramStringFormatting",
"//submodules/PresentationDataUtils",
"//submodules/Components/SheetComponent",
"//submodules/UndoUI",
"//submodules/TextFormat",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/SolidRoundedButtonComponent",
"//submodules/AvatarNode",
"//submodules/TelegramUI/Components/Stars/StarsImageComponent",
],
visibility = [
"//visibility:public",
],
)

View file

@ -2,6 +2,7 @@ import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
@ -29,6 +30,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
let action: () -> Void
let cancel: (Bool) -> Void
let openPeer: (EnginePeer) -> Void
let openMessage: (EngineMessage.Id) -> Void
let copyTransactionId: () -> Void
init(
@ -37,6 +39,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
action: @escaping () -> Void,
cancel: @escaping (Bool) -> Void,
openPeer: @escaping (EnginePeer) -> Void,
openMessage: @escaping (EngineMessage.Id) -> Void,
copyTransactionId: @escaping () -> Void
) {
self.context = context
@ -44,6 +47,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
self.action = action
self.cancel = cancel
self.openPeer = openPeer
self.openMessage = openMessage
self.copyTransactionId = copyTransactionId
}
@ -172,17 +176,23 @@ private final class StarsTransactionSheetContent: CombinedComponent {
let transactionId: String?
let date: Int32
let via: String?
let messageId: EngineMessage.Id?
let toPeer: EnginePeer?
let transactionPeer: StarsContext.State.Transaction.Peer?
let media: [Media]
let photo: TelegramMediaWebFile?
let isRefund: Bool
var delayedCloseOnOpenPeer = true
switch subject {
case let .transaction(transaction, isAccount):
case let .transaction(transaction, parentPeer):
switch transaction.peer {
case let .peer(peer):
titleText = transaction.title ?? peer.compactDisplayTitle
if !transaction.media.isEmpty {
titleText = strings.Stars_Transaction_MediaPurchase
} else {
titleText = transaction.title ?? peer.compactDisplayTitle
}
via = nil
case .appStore:
titleText = strings.Stars_Transaction_AppleTopUp_Title
@ -194,7 +204,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
titleText = strings.Stars_Transaction_PremiumBotTopUp_Title
via = strings.Stars_Transaction_PremiumBotTopUp_Subtitle
case .fragment:
if isAccount {
if parentPeer.id == component.context.account.peerId {
titleText = strings.Stars_Transaction_FragmentTopUp_Title
via = strings.Stars_Transaction_FragmentTopUp_Subtitle
} else {
@ -205,7 +215,49 @@ private final class StarsTransactionSheetContent: CombinedComponent {
titleText = strings.Stars_Transaction_Unsupported_Title
via = nil
}
descriptionText = transaction.description ?? ""
if !transaction.media.isEmpty {
var description: String = ""
var photoCount: Int = 0
var videoCount: Int = 0
for media in transaction.media {
if let _ = media as? TelegramMediaFile {
videoCount += 1
} else {
photoCount += 1
}
}
//TODO:localize
if photoCount > 0 && videoCount > 0 {
if photoCount > 1 {
description += "\(photoCount) photos**"
} else {
description += "**\(photoCount) photo**"
}
description += " and "
if videoCount > 1 {
description += "**\(videoCount) videos**"
} else {
description += "**\(videoCount) video**"
}
} else if photoCount > 0 {
if photoCount > 1 {
description += "**\(photoCount) photos**"
} else {
description += "**Photo**"
}
} else if videoCount > 0 {
if videoCount > 1 {
description += "**\(videoCount) videos**"
} else {
description += "**Video**"
}
}
descriptionText = description.replacingOccurrences(of: "**", with: "")
} else {
descriptionText = transaction.description ?? ""
}
messageId = transaction.paidMessageId
count = transaction.count
transactionId = transaction.id
@ -216,6 +268,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
toPeer = nil
}
transactionPeer = transaction.peer
media = transaction.media
photo = transaction.photo
isRefund = transaction.flags.contains(.isRefund)
case let .receipt(receipt):
@ -223,6 +276,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
descriptionText = receipt.invoiceMedia.description
count = (receipt.invoice.prices.first?.amount ?? receipt.invoiceMedia.totalAmount) * -1
via = nil
messageId = nil
transactionId = receipt.transactionId
date = receipt.date
if let peer = state.peerMap[receipt.botPaymentId] {
@ -231,6 +285,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
toPeer = nil
}
transactionPeer = nil
media = []
photo = receipt.invoiceMedia.photo
isRefund = false
delayedCloseOnOpenPeer = false
@ -261,7 +316,9 @@ private final class StarsTransactionSheetContent: CombinedComponent {
)
let imageSubject: StarsImageComponent.Subject
if let photo {
if !media.isEmpty {
imageSubject = .media(media)
} else if let photo {
imageSubject = .photo(photo)
} else if let transactionPeer {
imageSubject = .transactionPeer(transactionPeer)
@ -275,7 +332,8 @@ private final class StarsTransactionSheetContent: CombinedComponent {
context: component.context,
subject: imageSubject,
theme: theme,
diameter: 90.0
diameter: 90.0,
backgroundColor: theme.actionSheet.opaqueItemBackgroundColor
),
availableSize: CGSize(width: context.availableSize.width, height: 200.0),
transition: .immediate
@ -345,6 +403,41 @@ private final class StarsTransactionSheetContent: CombinedComponent {
)
))
}
if let messageId {
//TODO:localize
let peerName: String
if case let .transaction(_, parentPeer) = component.subject {
if parentPeer.id == component.context.account.peerId {
if let toPeer {
peerName = toPeer.addressName ?? "c/\(toPeer.id.id._internalGetInt64Value())"
} else {
peerName = ""
}
} else {
peerName = parentPeer.addressName ?? "c/\(parentPeer.id.id._internalGetInt64Value())"
}
} else {
peerName = ""
}
tableItems.append(.init(
id: "via",
title: strings.Stars_Transaction_Media,
component: AnyComponent(
Button(
content: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "t.me/\(peerName)/\(messageId.id)", font: tableFont, textColor: tableLinkColor)))
),
action: {
component.openMessage(messageId)
Queue.mainQueue().after(1.0, {
component.cancel(false)
})
}
)
)
))
}
if let transactionId {
tableItems.append(.init(
@ -544,6 +637,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
let subject: StarsTransactionScreen.Subject
let action: () -> Void
let openPeer: (EnginePeer) -> Void
let openMessage: (EngineMessage.Id) -> Void
let copyTransactionId: () -> Void
init(
@ -551,12 +645,14 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
subject: StarsTransactionScreen.Subject,
action: @escaping () -> Void,
openPeer: @escaping (EnginePeer) -> Void,
openMessage: @escaping (EngineMessage.Id) -> Void,
copyTransactionId: @escaping () -> Void
) {
self.context = context
self.subject = subject
self.action = action
self.openPeer = openPeer
self.openMessage = openMessage
self.copyTransactionId = copyTransactionId
}
@ -599,6 +695,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
}
},
openPeer: context.component.openPeer,
openMessage: context.component.openMessage,
copyTransactionId: context.component.copyTransactionId
)),
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
@ -667,7 +764,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
public class StarsTransactionScreen: ViewControllerComponentContainer {
public enum Subject: Equatable {
case transaction(StarsContext.State.Transaction, Bool)
case transaction(StarsContext.State.Transaction, EnginePeer)
case receipt(BotPaymentReceipt)
}
@ -685,6 +782,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
self.context = context
var openPeerImpl: ((EnginePeer) -> Void)?
var openMessageImpl: ((EngineMessage.Id) -> Void)?
var copyTransactionIdImpl: (() -> Void)?
super.init(
context: context,
@ -695,6 +793,9 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
openPeer: { peerId in
openPeerImpl?(peerId)
},
openMessage: { messageId in
openMessageImpl?(messageId)
},
copyTransactionId: {
copyTransactionIdImpl?()
}
@ -724,6 +825,23 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
})
}
openMessageImpl = { [weak self] messageId in
guard let self else {
return
}
let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)
)
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer else {
return
}
if let navigationController = self.navigationController as? NavigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always, useExisting: false, purposefulAction: {}, peekData: nil))
}
})
}
copyTransactionIdImpl = { [weak self] in
guard let self else {
return
@ -1210,3 +1328,24 @@ private final class TransactionCellComponent: Component {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(backgroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setLineWidth(2.0)
context.setLineCap(.round)
context.setStrokeColor(foregroundColor.cgColor)
context.move(to: CGPoint(x: 10.0, y: 10.0))
context.addLine(to: CGPoint(x: 20.0, y: 20.0))
context.strokePath()
context.move(to: CGPoint(x: 20.0, y: 10.0))
context.addLine(to: CGPoint(x: 10.0, y: 20.0))
context.strokePath()
})
}

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

@ -20,6 +20,7 @@ import UndoUI
import ListItemComponentAdaptor
import StatisticsUI
import ItemListUI
import StarsWithdrawalScreen
final class StarsStatisticsScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -644,8 +645,14 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer {
guard let self else {
return
}
let controller = context.sharedContext.makeStarsTransactionScreen(context: context, transaction: transaction, isAccount: false)
self.push(controller)
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
}
let controller = context.sharedContext.makeStarsTransactionScreen(context: context, transaction: transaction, peer: peer)
self.push(controller)
})
}
withdrawImpl = { [weak self] in
@ -666,7 +673,7 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer {
guard let self, let stats = state.stats else {
return
}
let controller = StarsWithdrawScreen(context: context, mode: .withdraw(stats), completion: { [weak self] amount in
let controller = self.context.sharedContext.makeStarsWithdrawalScreen(context: context, stats: stats, completion: { [weak self] amount in
guard let self else {
return
}

View file

@ -15,6 +15,7 @@ import TelegramStringFormatting
import AvatarNode
import BundleIconComponent
import PhotoResources
import StarsAvatarComponent
private extension StarsContext.State.Transaction {
var extendedId: String {
@ -207,7 +208,11 @@ final class StarsTransactionsListPanelComponent: Component {
var itemDate: String
switch item.peer {
case let .peer(peer):
if let title = item.title {
if !item.media.isEmpty {
//TODO:localize
itemTitle = "Media Purchase"
itemSubtitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
} else if let title = item.title {
itemTitle = title
itemSubtitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
} else {
@ -291,9 +296,9 @@ final class StarsTransactionsListPanelComponent: Component {
theme: environment.theme,
title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)),
contentInsets: UIEdgeInsets(top: 9.0, left: environment.containerInsets.left, bottom: 8.0, right: environment.containerInsets.right),
leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(AvatarComponent(context: component.context, theme: environment.theme, peer: item.peer, photo: item.photo))), false),
leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: item.peer, photo: item.photo, media: item.media))), false),
icon: nil,
accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(LabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))),
accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))),
action: { [weak self] _ in
guard let self, let component = self.component else {
return
@ -493,241 +498,3 @@ func cancelContextGestures(view: UIView) {
cancelContextGestures(view: subview)
}
}
private final class AvatarComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let peer: StarsContext.State.Transaction.Peer
let photo: TelegramMediaWebFile?
init(context: AccountContext, theme: PresentationTheme, peer: StarsContext.State.Transaction.Peer, photo: TelegramMediaWebFile?) {
self.context = context
self.theme = theme
self.peer = peer
self.photo = photo
}
static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.photo != rhs.photo {
return false
}
return true
}
final class View: UIView {
private let avatarNode: AvatarNode
private let backgroundView = UIImageView()
private let iconView = UIImageView()
private var imageNode: TransformImageNode?
private let fetchDisposable = MetaDisposable()
private var component: AvatarComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0))
super.init(frame: frame)
self.iconView.contentMode = .scaleAspectFit
self.addSubnode(self.avatarNode)
self.addSubview(self.backgroundView)
self.addSubview(self.iconView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.fetchDisposable.dispose()
}
func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let size = CGSize(width: 40.0, height: 40.0)
var iconInset: CGFloat = 3.0
var iconOffset: CGFloat = 0.0
switch component.peer {
case let .peer(peer):
if let photo = component.photo {
let imageNode: TransformImageNode
if let current = self.imageNode {
imageNode = current
} else {
imageNode = TransformImageNode()
imageNode.contentAnimations = [.subsequentUpdates]
self.addSubview(imageNode.view)
self.imageNode = imageNode
imageNode.setSignal(chatWebFileImage(account: component.context.account, file: photo))
self.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: component.context.account, userLocation: .other, image: photo).startStrict())
}
imageNode.frame = CGRect(origin: .zero, size: size)
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: size.width / 2.0), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
self.backgroundView.isHidden = true
self.iconView.isHidden = true
self.avatarNode.isHidden = true
} else {
self.avatarNode.setPeer(
context: component.context,
theme: component.theme,
peer: peer,
synchronousLoad: true
)
self.backgroundView.isHidden = true
self.iconView.isHidden = true
self.avatarNode.isHidden = false
}
case .appStore:
self.backgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0x2a9ef1).cgColor,
UIColor(rgb: 0x72d5fd).cgColor
],
direction: .mirroredDiagonal
)
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Apple")
case .playMarket:
self.backgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0x54cb68).cgColor,
UIColor(rgb: 0xa0de7e).cgColor
],
direction: .mirroredDiagonal
)
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Google")
case .fragment:
self.backgroundView.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1b1f24))
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment")
iconOffset = 2.0
case .premiumBot:
iconInset = 7.0
self.backgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0x6b93ff).cgColor,
UIColor(rgb: 0x6b93ff).cgColor,
UIColor(rgb: 0x8d77ff).cgColor,
UIColor(rgb: 0xb56eec).cgColor,
UIColor(rgb: 0xb56eec).cgColor
],
direction: .mirroredDiagonal
)
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white)
case .unsupported:
iconInset = 7.0
self.backgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0xb1b1b1).cgColor,
UIColor(rgb: 0xcdcdcd).cgColor
],
direction: .mirroredDiagonal
)
self.backgroundView.isHidden = false
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white)
}
self.avatarNode.frame = CGRect(origin: .zero, size: size)
self.iconView.frame = CGRect(origin: .zero, size: size).insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset)
self.backgroundView.frame = CGRect(origin: .zero, size: size)
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class LabelComponent: CombinedComponent {
let text: NSAttributedString
init(
text: NSAttributedString
) {
self.text = text
}
static func ==(lhs: LabelComponent, rhs: LabelComponent) -> Bool {
if lhs.text != rhs.text {
return false
}
return true
}
static var body: Body {
let text = Child(MultilineTextComponent.self)
let icon = Child(BundleIconComponent.self)
return { context in
let component = context.component
let text = text.update(
component: MultilineTextComponent(text: .plain(component.text)),
availableSize: CGSize(width: 100.0, height: 40.0),
transition: context.transition
)
let iconSize = CGSize(width: 20.0, height: 20.0)
let icon = icon.update(
component: BundleIconComponent(
name: "Premium/Stars/StarLarge",
tintColor: nil
),
availableSize: iconSize,
transition: context.transition
)
let spacing: CGFloat = 3.0
let totalWidth = text.size.width + spacing + iconSize.width
let size = CGSize(width: totalWidth, height: iconSize.height)
context.add(text
.position(CGPoint(x: text.size.width / 2.0, y: size.height / 2.0))
)
context.add(icon
.position(CGPoint(x: totalWidth - iconSize.width / 2.0, y: size.height / 2.0 - UIScreenPixel))
)
return size
}
}
}

View file

@ -723,8 +723,14 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
guard let self else {
return
}
let controller = context.sharedContext.makeStarsTransactionScreen(context: context, transaction: transaction, isAccount: true)
self.push(controller)
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
}
let controller = context.sharedContext.makeStarsTransactionScreen(context: context, transaction: transaction, peer: peer)
self.push(controller)
})
}
buyImpl = { [weak self] in

View file

@ -26,6 +26,7 @@ private final class SheetContent: CombinedComponent {
let starsContext: StarsContext
let invoice: TelegramMediaInvoice
let source: BotPaymentInvoiceSource
let extendedMedia: [TelegramExtendedMedia]
let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>
let dismiss: () -> Void
@ -34,6 +35,7 @@ private final class SheetContent: CombinedComponent {
starsContext: StarsContext,
invoice: TelegramMediaInvoice,
source: BotPaymentInvoiceSource,
extendedMedia: [TelegramExtendedMedia],
inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>,
dismiss: @escaping () -> Void
) {
@ -41,6 +43,7 @@ private final class SheetContent: CombinedComponent {
self.starsContext = starsContext
self.invoice = invoice
self.source = source
self.extendedMedia = extendedMedia
self.inputData = inputData
self.dismiss = dismiss
}
@ -52,6 +55,9 @@ private final class SheetContent: CombinedComponent {
if lhs.invoice != rhs.invoice {
return false
}
if lhs.extendedMedia != rhs.extendedMedia {
return false
}
return true
}
@ -64,7 +70,8 @@ private final class SheetContent: CombinedComponent {
private let source: BotPaymentInvoiceSource
private let invoice: TelegramMediaInvoice
private(set) var peer: EnginePeer?
private(set) var botPeer: EnginePeer?
private(set) var chatPeer: EnginePeer?
private var peerDisposable: Disposable?
private(set) var balance: Int64?
private(set) var form: BotPaymentForm?
@ -95,14 +102,25 @@ private final class SheetContent: CombinedComponent {
super.init()
self.peerDisposable = (inputData
|> deliverOnMainQueue).start(next: { [weak self] inputData in
let chatPeer: Signal<EnginePeer?, NoError>
if case let .message(messageId) = source {
chatPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId))
} else {
chatPeer = .single(nil)
}
self.peerDisposable = (combineLatest(
inputData,
chatPeer
)
|> deliverOnMainQueue).start(next: { [weak self] inputData, chatPeer in
guard let self else {
return
}
self.balance = inputData?.0.balance ?? 0
self.form = inputData?.1
self.peer = inputData?.2
self.botPeer = inputData?.2
self.chatPeer = chatPeer
self.updated(transition: .immediate)
if self.optionsDisposable == nil, let balance = self.balance, balance < self.invoice.totalAmount {
@ -228,9 +246,9 @@ private final class SheetContent: CombinedComponent {
)
let subject: StarsImageComponent.Subject
if let extendedMedia = component.invoice.extendedMedia {
subject = .extendedMedia(extendedMedia)
} else if let peer = state.peer {
if !component.extendedMedia.isEmpty {
subject = .extendedMedia(component.extendedMedia)
} else if let peer = state.botPeer {
if let photo = component.invoice.photo {
subject = .photo(photo)
} else {
@ -244,7 +262,8 @@ private final class SheetContent: CombinedComponent {
context: component.context,
subject: subject,
theme: theme,
diameter: 90.0
diameter: 90.0,
backgroundColor: theme.list.blocksBackgroundColor
),
availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0),
transition: context.transition
@ -299,14 +318,52 @@ private final class SheetContent: CombinedComponent {
let amount = component.invoice.totalAmount
let infoText: String
if let _ = component.invoice.extendedMedia {
if !component.extendedMedia.isEmpty {
//TODO:localize
var description: String = ""
var photoCount: Int = 0
var videoCount: Int = 0
for media in component.extendedMedia {
if case let .preview(_, _, videoDuration) = media, videoDuration != nil {
videoCount += 1
} else {
photoCount += 1
}
}
if photoCount > 0 && videoCount > 0 {
if photoCount > 1 {
description += "**\(photoCount) photos**"
} else {
description += "**\(photoCount) photo**"
}
description += " and "
if videoCount > 1 {
description += "**\(videoCount) videos**"
} else {
description += "**\(videoCount) video**"
}
} else if photoCount > 0 {
if photoCount > 1 {
description += "**\(photoCount) photos**"
} else {
description += "**photo**"
}
} else if videoCount > 0 {
if videoCount > 1 {
description += "**\(videoCount) videos**"
} else {
description += "**video**"
}
}
infoText = strings.Stars_Transfer_UnlockInfo(
description,
state.chatPeer?.compactDisplayTitle ?? "",
strings.Stars_Transfer_Info_Stars(Int32(amount))
).string
} else {
infoText = strings.Stars_Transfer_Info(
component.invoice.title,
state.peer?.compactDisplayTitle ?? "",
state.botPeer?.compactDisplayTitle ?? "",
strings.Stars_Transfer_Info_Stars(Int32(amount))
).string
}
@ -386,7 +443,7 @@ private final class SheetContent: CombinedComponent {
let accountContext = component.context
let starsContext = component.starsContext
let botTitle = state.peer?.compactDisplayTitle ?? ""
let botTitle = state.botPeer?.compactDisplayTitle ?? ""
let invoice = component.invoice
let button = button.update(
component: ButtonComponent(
@ -410,7 +467,7 @@ private final class SheetContent: CombinedComponent {
context: accountContext,
starsContext: starsContext,
options: state?.options ?? [],
peerId: state?.peer?.id,
peerId: state?.botPeer?.id,
requiredStars: invoice.totalAmount,
completion: { [weak starsContext] stars in
starsContext?.add(balance: stars)
@ -484,6 +541,7 @@ private final class StarsTransferSheetComponent: CombinedComponent {
private let starsContext: StarsContext
private let invoice: TelegramMediaInvoice
private let source: BotPaymentInvoiceSource
private let extendedMedia: [TelegramExtendedMedia]
private let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>
init(
@ -491,12 +549,14 @@ private final class StarsTransferSheetComponent: CombinedComponent {
starsContext: StarsContext,
invoice: TelegramMediaInvoice,
source: BotPaymentInvoiceSource,
extendedMedia: [TelegramExtendedMedia],
inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>
) {
self.context = context
self.starsContext = starsContext
self.invoice = invoice
self.source = source
self.extendedMedia = extendedMedia
self.inputData = inputData
}
@ -507,6 +567,9 @@ private final class StarsTransferSheetComponent: CombinedComponent {
if lhs.invoice != rhs.invoice {
return false
}
if lhs.extendedMedia != rhs.extendedMedia {
return false
}
return true
}
@ -526,6 +589,7 @@ private final class StarsTransferSheetComponent: CombinedComponent {
starsContext: context.component.starsContext,
invoice: context.component.invoice,
source: context.component.source,
extendedMedia: context.component.extendedMedia,
inputData: context.component.inputData,
dismiss: {
animateOut.invoke(Action { _ in
@ -584,6 +648,7 @@ public final class StarsTransferScreen: ViewControllerComponentContainer {
starsContext: StarsContext,
invoice: TelegramMediaInvoice,
source: BotPaymentInvoiceSource,
extendedMedia: [TelegramExtendedMedia],
inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>,
completion: @escaping (Bool) -> Void
) {
@ -597,6 +662,7 @@ public final class StarsTransferScreen: ViewControllerComponentContainer {
starsContext: starsContext,
invoice: invoice,
source: source,
extendedMedia: extendedMedia,
inputData: inputData
),
navigationBarAppearance: .none,

View file

@ -0,0 +1,44 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "StarsWithdrawalScreen",
module_name = "StarsWithdrawalScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/ItemListUI",
"//submodules/TelegramStringFormatting",
"//submodules/PresentationDataUtils",
"//submodules/Components/SheetComponent",
"//submodules/UndoUI",
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/ScrollComponent",
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/PasswordSetupUI",
"//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController",
],
visibility = [
"//visibility:public",
],
)

View file

@ -9,7 +9,7 @@ import PasswordSetupUI
import Markdown
import OwnershipTransferController
func confirmStarsRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, amount: Int64, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController {
public func confirmStarsRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, amount: Int64, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
var dismissImpl: (() -> Void)?
@ -73,7 +73,7 @@ func confirmStarsRevenueWithdrawalController(context: AccountContext, updatedPre
}
func starsRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, amount: Int64, initialError: RequestStarsRevenueWithdrawalError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController {
public func starsRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, amount: Int64, initialError: RequestStarsRevenueWithdrawalError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
let theme = AlertControllerTheme(presentationData: presentationData)

View file

@ -111,13 +111,14 @@ private final class SheetContent: CombinedComponent {
let minAmount: Int64?
let maxAmount: Int64?
let configuration = StarsWithdrawConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
switch component.mode {
case let .withdraw(status):
titleString = environment.strings.Stars_Withdraw_Title
amountTitle = environment.strings.Stars_Withdraw_AmountTitle
amountPlaceholder = environment.strings.Stars_Withdraw_AmountPlaceholder
let configuration = StarsWithdrawConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
minAmount = configuration.minWithdrawAmount
maxAmount = status.balances.availableBalance
case .paidMedia:
@ -126,7 +127,7 @@ private final class SheetContent: CombinedComponent {
amountPlaceholder = environment.strings.Stars_PaidContent_AmountPlaceholder
minAmount = 1
maxAmount = nil
maxAmount = configuration.maxPaidMediaAmount
}
let title = title.update(
@ -335,8 +336,11 @@ private final class SheetContent: CombinedComponent {
func makeState() -> State {
var amount: Int64?
if case let .withdraw(stats) = mode {
switch self.mode {
case let .withdraw(stats):
amount = stats.balances.availableBalance
case let .paidMedia(initialValue):
amount = initialValue
}
return State(context: self.context, amount: amount)
}
@ -432,7 +436,7 @@ private final class StarsWithdrawSheetComponent: CombinedComponent {
public final class StarsWithdrawScreen: ViewControllerComponentContainer {
public enum Mode: Equatable {
case withdraw(StarsRevenueStats)
case paidMedia
case paidMedia(Int64?)
}
private let context: AccountContext
@ -730,18 +734,29 @@ func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor
private struct StarsWithdrawConfiguration {
static var defaultValue: StarsWithdrawConfiguration {
return StarsWithdrawConfiguration(minWithdrawAmount: nil)
return StarsWithdrawConfiguration(minWithdrawAmount: nil, maxPaidMediaAmount: nil)
}
let minWithdrawAmount: Int64?
let maxPaidMediaAmount: Int64?
fileprivate init(minWithdrawAmount: Int64?) {
fileprivate init(minWithdrawAmount: Int64?, maxPaidMediaAmount: Int64?) {
self.minWithdrawAmount = minWithdrawAmount
self.maxPaidMediaAmount = maxPaidMediaAmount
}
static func with(appConfiguration: AppConfiguration) -> StarsWithdrawConfiguration {
if let data = appConfiguration.data, let minWithdrawAmount = data["stars_revenue_withdrawal_min"] as? Double {
return StarsWithdrawConfiguration(minWithdrawAmount: Int64(minWithdrawAmount))
if let data = appConfiguration.data {
var minWithdrawAmount: Int64?
if let value = data["stars_revenue_withdrawal_min"] as? Double {
minWithdrawAmount = Int64(value)
}
var maxPaidMediaAmount: Int64?
if let value = data["stars_paid_post_amount_max"] as? Double {
maxPaidMediaAmount = Int64(value)
}
return StarsWithdrawConfiguration(minWithdrawAmount: minWithdrawAmount, maxPaidMediaAmount: maxPaidMediaAmount)
} else {
return .defaultValue
}

View file

@ -742,7 +742,7 @@ public class StickerPickerScreen: ViewController {
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
let gallery = GalleryController(context: context, source: .standaloneMessage(message), streamSingleVideo: true, replaceRootController: { _, _ in
let gallery = GalleryController(context: context, source: .standaloneMessage(message, nil), streamSingleVideo: true, replaceRootController: { _, _ in
}, baseNavigationController: nil)
gallery.setHintWillBePresentedInPreviewingContext(true)

View file

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

View file

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

View file

@ -901,42 +901,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
}
// #if DEBUG
// if message.text == "#", let telegramImage = message.media.first(where: { $0 is TelegramMediaImage }) as? TelegramMediaImage {
// let invoice = TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: "XTR", totalAmount: 100, startParam: "", extendedMedia: .preview(dimensions: telegramImage.representations.first?.dimensions ?? PixelDimensions(width: 1, height: 1), immediateThumbnailData: telegramImage.immediateThumbnailData, videoDuration: nil), flags: [], version: 0)
//
// let inputData = Promise<BotCheckoutController.InputData?>()
// inputData.set(.single(
// BotCheckoutController.InputData(
// form: BotPaymentForm(id: 123, canSaveCredentials: false, passwordMissing: false, invoice: BotPaymentInvoice(isTest: false, requestedFields: [], currency: "XTR", prices: [BotPaymentPrice(label: "", amount: 100)], tip: nil, termsInfo: nil), paymentBotId: message.id.peerId, providerId: nil, url: nil, nativeProvider: nil, savedInfo: nil, savedCredentials: [], additionalPaymentMethods: []),
// validatedFormInfo: nil,
// botPeer: nil
// )))
// if invoice.currency == "XTR", let starsContext = strongSelf.context.starsContext {
// let starsInputData = combineLatest(
// inputData.get(),
// starsContext.state
// )
// |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in
// if let data, let state {
// return (state, data.form, data.botPeer)
// } else {
// return nil
// }
// }
// let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
// guard let strongSelf = self else {
// return
// }
// let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .message(message.id), inputData: starsInputData, completion: { _ in })
// strongSelf.push(controller)
// })
// }
// return true
// }
// #endif
if let paidContent = media as? TelegramMediaPaidContent, let extendedMedia = paidContent.extendedMedia.first {
switch extendedMedia {
case .preview:
@ -1291,7 +1255,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
standalone = true
}
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: openChatLocation, chatFilterTag: chatFilterTag, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, standalone: standalone, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: {
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: openChatLocation, chatFilterTag: chatFilterTag, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, mediaIndex: params.mediaIndex, standalone: standalone, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: {
self?.chatDisplayNode.dismissInput()
}, present: { c, a in
self?.present(c, in: .window(.root), with: a, blockInteraction: true)
@ -2670,7 +2634,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
let invoice = TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: "XTR", totalAmount: paidContent.amount, startParam: "", extendedMedia: .preview(dimensions:dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: nil), flags: [], version: 0)
let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .message(messageId), inputData: starsInputData, completion: { _ in })
let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .message(messageId), extendedMedia: paidContent.extendedMedia, inputData: starsInputData, completion: { _ in })
strongSelf.push(controller)
})
}
@ -2711,7 +2675,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self else {
return
}
let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .message(messageId), inputData: starsInputData, completion: { _ in })
let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .message(messageId), extendedMedia: [], inputData: starsInputData, completion: { _ in })
strongSelf.push(controller)
})
} else {

View file

@ -638,6 +638,30 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
imageDimensions = dimensions.cgSize
}
break
} else if let paidContent = media as? TelegramMediaPaidContent, let firstMedia = paidContent.extendedMedia.first {
switch firstMedia {
case let .preview(dimensions, immediateThumbnailData, _):
let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
if let dimensions {
imageDimensions = dimensions.cgSize
}
updatedMediaReference = .standalone(media: thumbnailMedia)
case let .full(fullMedia):
updatedMediaReference = .message(message: MessageReference(message), media: fullMedia)
if let image = fullMedia as? TelegramMediaImage {
if let representation = largestRepresentationForPhoto(image) {
imageDimensions = representation.dimensions.cgSize
}
break
} else if let file = fullMedia as? TelegramMediaFile {
if !file.isInstantVideo && !file.isSticker, let representation = largestImageRepresentation(file.previewRepresentations) {
imageDimensions = representation.dimensions.cgSize
} else if file.isAnimated, let dimensions = file.dimensions {
imageDimensions = dimensions.cgSize
}
break
}
}
}
}
}
@ -681,7 +705,11 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
if mediaUpdated {
if let updatedMediaReference = updatedMediaReference, imageDimensions != nil {
if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) {
updateImageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, blurred: hasSpoiler)
if imageReference.media.representations.isEmpty {
updateImageSignal = chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, ignoreFullSize: true, synchronousLoad: true)
} else {
updateImageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, blurred: hasSpoiler)
}
} else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) {
if fileReference.media.isAnimatedSticker {
let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512)

View file

@ -127,7 +127,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
return true
}
if let mediaData = chatMessageGalleryControllerData(context: params.context, chatLocation: params.chatLocation, chatFilterTag: params.chatFilterTag, chatLocationContextHolder: params.chatLocationContextHolder, message: params.message, navigationController: params.navigationController, standalone: params.standalone, reverseMessageGalleryOrder: params.reverseMessageGalleryOrder, mode: params.mode, source: params.gallerySource, synchronousLoad: false, actionInteraction: params.actionInteraction) {
if let mediaData = chatMessageGalleryControllerData(context: params.context, chatLocation: params.chatLocation, chatFilterTag: params.chatFilterTag, chatLocationContextHolder: params.chatLocationContextHolder, message: params.message, mediaIndex: params.mediaIndex, navigationController: params.navigationController, standalone: params.standalone, reverseMessageGalleryOrder: params.reverseMessageGalleryOrder, mode: params.mode, source: params.gallerySource, synchronousLoad: false, actionInteraction: params.actionInteraction) {
switch mediaData {
case let .url(url):
params.openUrl(url)

View file

@ -837,7 +837,7 @@ func openResolvedUrlImpl(
}
}
let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { _ in
let controller = context.sharedContext.makeStarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: .slug(slug), inputData: starsInputData, completion: { _ in })
let controller = context.sharedContext.makeStarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: .slug(slug), extendedMedia: [], inputData: starsInputData, completion: { _ in })
navigationController.pushViewController(controller)
})
} else {
@ -1072,7 +1072,7 @@ func openResolvedUrlImpl(
},
openMessage: { messageId in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId))
|> deliverOnMainQueue).startStandalone(next: { peer in
|> deliverOnMainQueue).startStandalone(next: { peer in
guard let peer else {
return
}

View file

@ -67,6 +67,8 @@ import BirthdayPickerScreen
import StarsTransactionsScreen
import StarsPurchaseScreen
import StarsTransferScreen
import StarsTransactionScreen
import StarsWithdrawalScreen
private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>()
@ -2072,6 +2074,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
mappedSource = .messageTags
case .folderTags:
mappedSource = .folderTags
case .messageEffects:
mappedSource = .messageEffects
case .animatedEmoji:
mappedSource = .animatedEmoji
}
@ -2128,6 +2132,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
mappedSubject = .messagePrivacy
case .folderTags:
mappedSubject = .folderTags
case .messageEffects:
mappedSubject = .messageEffects
case .business:
mappedSubject = .business
buttonText = presentationData.strings.Chat_EmptyStateIntroFooterPremiumActionButton
@ -2628,12 +2634,12 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return StarsPurchaseScreen(context: context, starsContext: starsContext, options: options, peerId: peerId, requiredStars: requiredStars, modal: true, completion: completion)
}
public func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController {
return StarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: source, inputData: inputData, completion: completion)
public func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController {
return StarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: source, extendedMedia: extendedMedia, inputData: inputData, completion: completion)
}
public func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, isAccount: Bool) -> ViewController {
return StarsTransactionScreen(context: context, subject: .transaction(transaction, isAccount), action: {})
public func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, peer: EnginePeer) -> ViewController {
return StarsTransactionScreen(context: context, subject: .transaction(transaction, peer), action: {})
}
public func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController {
@ -2644,8 +2650,12 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return StarsStatisticsScreen(context: context, peerId: peerId, revenueContext: revenueContext)
}
public func makeStarsAmountScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController {
return StarsWithdrawScreen(context: context, mode: .paidMedia, completion: completion)
public func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController {
return StarsWithdrawScreen(context: context, mode: .paidMedia(initialValue), completion: completion)
}
public func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController {
return StarsWithdrawScreen(context: context, mode: .withdraw(stats), completion: completion)
}
}

View file

@ -882,6 +882,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
starsContext: starsContext,
invoice: invoice,
source: .slug(slug),
extendedMedia: [],
inputData: starsInputData,
completion: { [weak self] paid in
guard let self else {