From cdd987e627403d7d11ad340655fff3c46a3db956 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 7 Mar 2025 21:56:17 +0400 Subject: [PATCH 01/19] Various fixes --- .../AuthorizationSequenceController.swift | 2 +- .../BrowserUI/Sources/BrowserWebContent.swift | 4 +- .../ChannelPermissionsController.swift | 34 ++++++------ .../IncomingMessagePrivacyScreen.swift | 2 +- .../TelegramEngine/Payments/StarGifts.swift | 49 ++++++++++++++--- .../Sources/ServiceMessageStrings.swift | 2 +- .../Sources/ChatMessageBubbleItemNode.swift | 24 +++++++-- .../ChatMessageCommentFooterContentNode.swift | 2 +- .../ChatMessageGiftBubbleContentNode.swift | 7 +-- .../Sources/ChatSendStarsScreen.swift | 19 ++++++- .../Sources/GiftTransferAlertController.swift | 4 +- .../Sources/GiftViewScreen.swift | 34 +++++++++--- .../Sources/GiftWithdrawAlertController.swift | 2 +- .../Sources/MessageInputPanelComponent.swift | 2 +- .../Sources/MessagePriceItem.swift | 15 ++++-- .../Sources/PeerInfoGiftsCoverComponent.swift | 2 +- .../Sources/PeerInfoHeaderNode.swift | 1 + .../Sources/PeerInfoScreen.swift | 6 ++- .../GiftContextPreviewController.swift | 2 +- .../Sources/PeerInfoGiftsPaneNode.swift | 52 +++++++------------ .../Sources/StarsTransactionScreen.swift | 2 +- .../StarsTransactionsListPanelComponent.swift | 2 +- .../Sources/SharedAccountContext.swift | 2 +- submodules/WebUI/Sources/WebAppWebView.swift | 4 +- versions.json | 2 +- 25 files changed, 182 insertions(+), 95 deletions(-) diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index cb4520a32f..293e7475b9 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -351,7 +351,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth |> deliverOnMainQueue).startStrict(error: { [weak self] error in if let self, case .alreadyInProgress = error { let formattedNumber = formatPhoneNumber(number) - let title = NSAttributedString(string: self.presentationData.strings.Login_Email_PremiumRequiredTitle, font: Font.semibold(self.presentationData.listsFontSize.baseDisplaySize), textColor: self.presentationData.theme.actionSheet.primaryTextColor) + let title = NSAttributedString(string: self.presentationData.strings.Login_Email_PremiumRequiredTitle, font: Font.semibold(self.presentationData.listsFontSize.baseDisplaySize), textColor: self.presentationData.theme.actionSheet.primaryTextColor, paragraphAlignment: .center) let text = parseMarkdownIntoAttributedString(self.presentationData.strings.Login_Email_PremiumRequiredText(formattedNumber).string, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil }), textAlignment: .center).mutableCopy() as! NSMutableAttributedString let alertController = textWithEntitiesAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: title, text: text, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_OK, action: { })]) diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 4866b8d284..3ffa7a7d6a 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -1722,10 +1722,10 @@ function tgBrowserHandleMutations(mutations) { if (mutation.addedNodes && mutation.addedNodes.length > 0) { mutation.addedNodes.forEach((newNode) => { if (newNode.tagName === 'VIDEO') { - disableWebkitEnterFullscreen(newNode); + tgBrowserDisableWebkitEnterFullscreen(newNode); } if (newNode.querySelectorAll) { - newNode.querySelectorAll('video').forEach(disableWebkitEnterFullscreen); + newNode.querySelectorAll('video').forEach(tgBrowserDisableWebkitEnterFullscreen); } }); } diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift index 86c02ce352..e278d77c7a 100644 --- a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift @@ -37,10 +37,10 @@ private final class ChannelPermissionsControllerArguments { let openChannelExample: () -> Void let updateSlowmode: (Int32) -> Void let updateUnrestrictBoosters: (Int32) -> Void - let updateStarsAmount: (StarsAmount?) -> Void + let updateStarsAmount: (StarsAmount?, Bool) -> Void let toggleIsOptionExpanded: (TelegramChatBannedRightsFlags) -> Void - init(context: AccountContext, updatePermission: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (EnginePeer.Id) -> Void, openPeer: @escaping (ChannelParticipant) -> Void, openPeerInfo: @escaping (EnginePeer) -> Void, openKicked: @escaping () -> Void, presentRestrictedPermissionAlert: @escaping (TelegramChatBannedRightsFlags) -> Void, presentConversionToBroadcastGroup: @escaping () -> Void, openChannelExample: @escaping () -> Void, updateSlowmode: @escaping (Int32) -> Void, updateUnrestrictBoosters: @escaping (Int32) -> Void, updateStarsAmount: @escaping (StarsAmount?) -> Void, toggleIsOptionExpanded: @escaping (TelegramChatBannedRightsFlags) -> Void) { + init(context: AccountContext, updatePermission: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (EnginePeer.Id) -> Void, openPeer: @escaping (ChannelParticipant) -> Void, openPeerInfo: @escaping (EnginePeer) -> Void, openKicked: @escaping () -> Void, presentRestrictedPermissionAlert: @escaping (TelegramChatBannedRightsFlags) -> Void, presentConversionToBroadcastGroup: @escaping () -> Void, openChannelExample: @escaping () -> Void, updateSlowmode: @escaping (Int32) -> Void, updateUnrestrictBoosters: @escaping (Int32) -> Void, updateStarsAmount: @escaping (StarsAmount?, Bool) -> Void, toggleIsOptionExpanded: @escaping (TelegramChatBannedRightsFlags) -> Void) { self.context = context self.updatePermission = updatePermission self.addPeer = addPeer @@ -418,15 +418,15 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry { } case let .chargeForMessages(_, title, value): return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in - arguments.updateStarsAmount(value ? StarsAmount(value: 400, nanos: 0) : nil) + arguments.updateStarsAmount(value ? StarsAmount(value: 400, nanos: 0) : nil, true) }) case let .chargeForMessagesInfo(_, value): return ItemListTextItem(presentationData: presentationData, text: .plain(value), sectionId: self.section) case let .messagePriceHeader(_, value): return ItemListSectionHeaderItem(presentationData: presentationData, text: value, sectionId: self.section) case let .messagePrice(_, value, maxValue, price): - return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, isEnabled: true, minValue: 1, maxValue: maxValue, value: value, price: price, sectionId: self.section, updated: { value in - arguments.updateStarsAmount(StarsAmount(value: value, nanos: 0)) + return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, isEnabled: true, minValue: 1, maxValue: maxValue, value: value, price: price, sectionId: self.section, updated: { value, apply in + arguments.updateStarsAmount(StarsAmount(value: value, nanos: 0), apply) }) case let .messagePriceInfo(_, value): return ItemListTextItem(presentationData: presentationData, text: .plain(value), sectionId: self.section) @@ -1248,23 +1248,25 @@ public func channelPermissionsController(context: AccountContext, updatedPresent updateUnrestrictBoostersDisposable.set((context.engine.peers.updateChannelBoostsToUnlockRestrictions(peerId: view.peerId, boosts: value) |> deliverOnMainQueue).start()) }) - }, updateStarsAmount: { value in + }, updateStarsAmount: { value, apply in updateState { state in var state = state state.modifiedStarsAmount = value return state } - let _ = (peerView.get() - |> take(1) - |> deliverOnMainQueue).start(next: { view in - var effectiveValue = value - if value?.value == 0 { - effectiveValue = nil - } - updateSendPaidMessageStarsDisposable.set((context.engine.peers.updateChannelPaidMessagesStars(peerId: view.peerId, stars: effectiveValue) - |> deliverOnMainQueue).start()) - }) + if apply { + let _ = (peerView.get() + |> take(1) + |> deliverOnMainQueue).start(next: { view in + var effectiveValue = value + if value?.value == 0 { + effectiveValue = nil + } + updateSendPaidMessageStarsDisposable.set((context.engine.peers.updateChannelPaidMessagesStars(peerId: view.peerId, stars: effectiveValue) + |> deliverOnMainQueue).start()) + }) + } }, toggleIsOptionExpanded: { flags in updateState { state in var state = state diff --git a/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift index 3ab7c7b914..090e6672ed 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift @@ -149,7 +149,7 @@ private enum GlobalAutoremoveEntry: ItemListNodeEntry { case .priceHeader: return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.Privacy_Messages_MessagePrice, sectionId: self.section) case let .price(value, maxValue, price, isEnabled): - return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, isEnabled: isEnabled, minValue: 1, maxValue: maxValue, value: value, price: price, sectionId: self.section, updated: { value in + return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, isEnabled: isEnabled, minValue: 1, maxValue: maxValue, value: value, price: price, sectionId: self.section, updated: { value, _ in arguments.updateValue(.paidMessages(StarsAmount(value: value, nanos: 0))) }, openPremiumInfo: { arguments.openPremiumInfo() diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 203b53e543..72c1af1dc3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -1016,7 +1016,10 @@ private final class ProfileGiftsContextImpl { let sorting = self.sorting let isFiltered = self.filter != .All || self.sorting != .date - + if !isFiltered { + self.filteredGifts = [] + self.filteredCount = nil + } let dataState = isFiltered ? self.filteredDataState : self.dataState if case let .ready(true, initialNextOffset) = dataState { @@ -1202,7 +1205,6 @@ private final class ProfileGiftsContextImpl { } } let existingGifts = Set(pinnedGifts.compactMap { $0.reference }) - var updatedGifts: [ProfileGiftsContext.State.StarGift] = [] for gift in self.gifts { if let reference = gift.reference, existingGifts.contains(reference) { @@ -1216,18 +1218,44 @@ private final class ProfileGiftsContextImpl { updatedGifts.insert(contentsOf: pinnedGifts, at: 0) self.gifts = updatedGifts - if let index = self.filteredGifts.firstIndex(where: { $0.reference == reference }) { - self.filteredGifts[index] = self.filteredGifts[index].withPinnedToTop(pinnedToTop) + var effectiveReferences = pinnedGifts.compactMap { $0.reference } + if !self.filteredGifts.isEmpty { + var filteredPinnedGifts = self.filteredGifts.filter { $0.pinnedToTop } + if var gift = self.filteredGifts.first(where: { $0.reference == reference }) { + gift = gift.withPinnedToTop(pinnedToTop) + if pinnedToTop { + if !gift.savedToProfile { + gift = gift.withSavedToProfile(true) + } + filteredPinnedGifts.append(gift) + } else { + filteredPinnedGifts.removeAll(where: { $0.reference == reference }) + } + } + let existingFilteredGifts = Set(filteredPinnedGifts.compactMap { $0.reference }) + var updatedFilteredGifts: [ProfileGiftsContext.State.StarGift] = [] + for gift in self.filteredGifts { + if let reference = gift.reference, existingFilteredGifts.contains(reference) { + continue + } + updatedFilteredGifts.append(gift) + } + updatedFilteredGifts.sort { lhs, rhs in + lhs.date > rhs.date + } + updatedFilteredGifts.insert(contentsOf: filteredPinnedGifts, at: 0) + self.filteredGifts = updatedFilteredGifts + + effectiveReferences = filteredPinnedGifts.compactMap { $0.reference } } + self.pushState() - var signal = _internal_updateStarGiftsPinnedToTop(account: self.account, peerId: self.peerId, references: pinnedGifts.compactMap { $0.reference }) - + var signal = _internal_updateStarGiftsPinnedToTop(account: self.account, peerId: self.peerId, references: effectiveReferences) if saveToProfile { signal = _internal_updateStarGiftAddedToProfile(account: self.account, reference: reference, added: true) |> then(signal) } - self.actionDisposable.set( (signal |> deliverOn(self.queue)).startStrict(completed: { [weak self] in self?.reload() @@ -1279,7 +1307,6 @@ private final class ProfileGiftsContextImpl { |> ignoreValues |> then(signal) } - self.actionDisposable.set( (signal |> deliverOn(self.queue)).startStrict(completed: { [weak self] in self?.reload() @@ -1349,6 +1376,9 @@ private final class ProfileGiftsContextImpl { } func updateFilter(_ filter: ProfileGiftsContext.Filters) { + guard self.filter != filter else { + return + } self.filter = filter self.filteredDataState = .ready(canLoadMore: true, nextOffset: nil) self.pushState() @@ -1357,6 +1387,9 @@ private final class ProfileGiftsContextImpl { } func updateSorting(_ sorting: ProfileGiftsContext.Sorting) { + guard self.sorting != sorting else { + return + } self.sorting = sorting self.filteredDataState = .ready(canLoadMore: true, nextOffset: nil) self.pushState() diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index fdf187168a..89a13d50e5 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1148,7 +1148,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, peerId, senderId, _): if case let .unique(gift) = gift { if !forAdditionalServiceMessage && !"".isEmpty { - attributedString = NSAttributedString(string: "\(gift.title) #\(gift.number)", font: titleFont, textColor: primaryTextColor) + attributedString = NSAttributedString(string: "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))", font: titleFont, textColor: primaryTextColor) } else if let messagePeer = message.peers[message.id.peerId] { var peerName = EnginePeer(messagePeer).compactDisplayTitle var peerIds: [(Int, EnginePeer.Id?)] = [(0, messagePeer.id)] diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index df4b6cc9fa..ffa7a26e05 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -2968,7 +2968,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } else { let contentProperties = contentPropertiesAndLayouts[i].3 - if i == 0 && !headerSize.height.isZero { + if (i == 0 || (i == 1 && detachedContentNodesHeight > 0)) && !headerSize.height.isZero { if contentGroupId == nil { contentNodesHeight += properties.headerSpacing } @@ -2981,7 +2981,15 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if !contentContainerNodeFrames.isEmpty { overlapOffset = currentContainerGroupOverlap } - let containerFrame = CGRect(x: 0.0, y: headerSize.height + totalContentNodesHeight - contentNodesHeight - overlapOffset, width: maxContentWidth, height: contentNodesHeight) + var containerContentNodesOrigin = contentNodesHeight + var containerContentNodesHeight = contentNodesHeight + if detachedContentNodesHeight > 0 { + if contentContainerNodeFrames.isEmpty { + containerContentNodesHeight -= detachedContentNodesHeight - 4.0 + containerContentNodesOrigin -= detachedContentNodesHeight - 4.0 + } + } + let containerFrame = CGRect(x: 0.0, y: headerSize.height + totalContentNodesHeight - containerContentNodesOrigin - overlapOffset, width: maxContentWidth, height: containerContentNodesHeight) contentContainerNodeFrames.append((containerGroupId, containerFrame, currentItemSelection, currentContainerGroupOverlap)) if !overlapOffset.isZero { @@ -2997,7 +3005,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } var contentNodeOriginY = contentNodesHeight - if detachedContentNodesHeight > 0 { + if detachedContentNodesHeight > 0, contentContainerNodeFrames.isEmpty { contentNodeOriginY -= detachedContentNodesHeight - 4.0 } @@ -3025,7 +3033,15 @@ 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)) + var containerContentNodesOrigin = contentNodesHeight + var containerContentNodesHeight = contentNodesHeight + if detachedContentNodesHeight > 0 { + if contentContainerNodeFrames.isEmpty { + containerContentNodesHeight -= detachedContentNodesHeight - 4.0 + containerContentNodesOrigin -= detachedContentNodesHeight - 4.0 + } + } + contentContainerNodeFrames.append((containerGroupId, CGRect(x: 0.0, y: headerSize.height + totalContentNodesHeight - containerContentNodesOrigin - overlapOffset, width: maxContentWidth, height: containerContentNodesHeight), currentItemSelection, currentContainerGroupOverlap)) if !overlapOffset.isZero { totalContentNodesHeight -= currentContainerGroupOverlap } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageCommentFooterContentNode/Sources/ChatMessageCommentFooterContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageCommentFooterContentNode/Sources/ChatMessageCommentFooterContentNode.swift index 5bd1a66e68..99d4e8c196 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageCommentFooterContentNode/Sources/ChatMessageCommentFooterContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageCommentFooterContentNode/Sources/ChatMessageCommentFooterContentNode.swift @@ -213,7 +213,7 @@ public final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContent } else { textLeftInset = 15.0 + imageSize * min(1.0, CGFloat(replyPeers.count)) + (imageSpacing) * max(0.0, min(2.0, CGFloat(replyPeers.count - 1))) } - let textRightInset: CGFloat = 36.0 + let textRightInset: CGFloat = 24.0 let textConstrainedSize = CGSize(width: min(maxTextWidth, constrainedSize.width - horizontalInset - textLeftInset - textRightInset), height: constrainedSize.height) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 5a38e4a5e2..bf9eb2c51a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -354,8 +354,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: EngineMessage(item.message), accountPeerId: item.context.account.peerId) - let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText - + var primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText + var months: Int32 = 3 var animationName: String = "" var animationFile: TelegramMediaFile? @@ -582,7 +582,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } else { title = isStoryEntity ? uniqueGift.title : item.presentationData.strings.Notification_StarGift_Title(authorName).string } - text = isStoryEntity ? "**\(item.presentationData.strings.Notification_StarGift_Collectible) #\(uniqueGift.number)**" : "**\(uniqueGift.title) #\(uniqueGift.number)**" + text = isStoryEntity ? "**\(item.presentationData.strings.Notification_StarGift_Collectible) #\(presentationStringsFormattedNumber(uniqueGift.number, item.presentationData.dateTimeFormat.groupingSeparator))**" : "**\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, item.presentationData.dateTimeFormat.groupingSeparator))**" ribbonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_Gift buttonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_View modelTitle = item.presentationData.strings.Notification_StarGift_Model @@ -599,6 +599,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { uniqueSecondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColor)) uniquePatternColor = UIColor(rgb: UInt32(bitPattern: patternColor)) backdropValue = name + primaryTextColor = UIColor(rgb: 0xffffff) subtitleColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3) case let .pattern(name, file, _): symbolValue = name diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index 3f3e47ca50..f3793bdce4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -1441,14 +1441,31 @@ private final class ChatSendStarsScreenComponent: Component { } let sideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5) + 16.0 + let context = component.context let balanceSize = self.balanceOverlay.update( transition: .immediate, component: AnyComponent( StarsBalanceOverlayComponent( context: component.context, theme: environment.theme, - action: { + action: { [weak self] in + guard let self, let starsContext = context.starsContext, let navigationController = self.environment?.controller()?.navigationController as? NavigationController else { + return + } + self.environment?.controller()?.dismiss() + let _ = (context.engine.payments.starsTopUpOptions() + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { options in + let controller = context.sharedContext.makeStarsPurchaseScreen( + context: context, + starsContext: starsContext, + options: options, + purpose: .generic, + completion: { _ in } + ) + navigationController.pushViewController(controller) + }) } ) ), diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift index a9ff187f51..ffa0d3ffb8 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift @@ -259,10 +259,10 @@ public func giftTransferAlertController(context: AccountContext, gift: StarGift. let text: String let buttonText: String if transferStars > 0 { - text = strings.Gift_Transfer_Confirmation_Text("\(gift.title) #\(gift.number)", peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder), strings.Gift_Transfer_Confirmation_Text_Stars(Int32(transferStars))).string + text = strings.Gift_Transfer_Confirmation_Text("\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))", peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder), strings.Gift_Transfer_Confirmation_Text_Stars(Int32(transferStars))).string buttonText = "\(strings.Gift_Transfer_Confirmation_Transfer) $ \(transferStars)" } else { - text = strings.Gift_Transfer_Confirmation_TextFree("\(gift.title) #\(gift.number)", peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)).string + text = strings.Gift_Transfer_Confirmation_TextFree("\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))", peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)).string buttonText = strings.Gift_Transfer_Confirmation_TransferFree } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 0beadb7f97..9d2c630241 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -635,7 +635,8 @@ private final class GiftViewSheetContent: CombinedComponent { if case .wearPreview = component.subject { giftTitle = uniqueGift.title } else { - giftTitle = "\(uniqueGift.title) #\(uniqueGift.number)" + + giftTitle = "\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))" } let wearTitle = wearTitle.update( @@ -1018,7 +1019,7 @@ private final class GiftViewSheetContent: CombinedComponent { var descriptionText: String if let uniqueGift { titleString = uniqueGift.title - descriptionText = "\(strings.Gift_Unique_Collectible) #\(uniqueGift.number)" + descriptionText = "\(strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))" } else if soldOut { descriptionText = strings.Gift_View_UnavailableDescription } else if upgraded { @@ -1480,7 +1481,7 @@ private final class GiftViewSheetContent: CombinedComponent { if isWearing { state.commitTakeOff() - component.showAttributeInfo(statusTag, strings.Gift_View_TookOff("\(uniqueGift.title) #\(uniqueGift.number)").string) + component.showAttributeInfo(statusTag, strings.Gift_View_TookOff("\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))").string) } else { if let controller = controller() as? GiftViewScreen { controller.dismissAllTooltips() @@ -1507,7 +1508,7 @@ private final class GiftViewSheetContent: CombinedComponent { state.requestWearPreview() } else { state.commitWear(uniqueGift) - component.showAttributeInfo(statusTag, strings.Gift_View_PutOn("\(uniqueGift.title) #\(uniqueGift.number)").string) + component.showAttributeInfo(statusTag, strings.Gift_View_PutOn("\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))").string) } }) } @@ -2090,7 +2091,7 @@ private final class GiftViewSheetContent: CombinedComponent { component.cancel(true) } else { Queue.mainQueue().after(0.2) { - component.showAttributeInfo(statusTag, strings.Gift_View_PutOn("\(uniqueGift.title) #\(uniqueGift.number)").string) + component.showAttributeInfo(statusTag, strings.Gift_View_PutOn("\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))").string) } } } @@ -3091,15 +3092,32 @@ public class GiftViewScreen: ViewControllerComponentContainer { super.containerLayoutUpdated(layout, transition: transition) if self.showBalance { + let context = self.context let insets = layout.insets(options: .statusBar) let balanceSize = self.balanceOverlay.update( transition: .immediate, component: AnyComponent( StarsBalanceOverlayComponent( - context: self.context, - theme: self.context.sharedContext.currentPresentationData.with { $0 }.theme, - action: { + context: context, + theme: context.sharedContext.currentPresentationData.with { $0 }.theme, + action: { [weak self] in + guard let self, let starsContext = context.starsContext, let navigationController = self.navigationController as? NavigationController else { + return + } + self.dismissAnimated() + let _ = (context.engine.payments.starsTopUpOptions() + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { options in + let controller = context.sharedContext.makeStarsPurchaseScreen( + context: context, + starsContext: starsContext, + options: options, + purpose: .generic, + completion: { _ in } + ) + navigationController.pushViewController(controller) + }) } ) ), diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift index 6048815011..6fb6a9d67f 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift @@ -274,7 +274,7 @@ public func giftWithdrawAlertController(context: AccountContext, gift: StarGift. let strings = presentationData.strings let title = strings.Gift_Withdraw_Title - let text = strings.Gift_Withdraw_Text("\(gift.title) #\(gift.number)").string + let text = strings.Gift_Withdraw_Text("\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))").string let buttonText = strings.Gift_Withdraw_Proceed var dismissImpl: ((Bool) -> Void)? diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 290e126b5e..01a4e252f0 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -681,7 +681,7 @@ public final class MessageInputPanelComponent: Component { if self.contextQueryPeer == nil, let peerId = component.chatLocation?.peerId { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self, peer?.addressName != nil else { + guard let self, let peer, case .channel = peer, peer.addressName != nil else { return } self.contextQueryPeer = peer diff --git a/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/Sources/MessagePriceItem.swift b/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/Sources/MessagePriceItem.swift index 598a80f7e0..eafde47eeb 100644 --- a/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/Sources/MessagePriceItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/Sources/MessagePriceItem.swift @@ -25,10 +25,10 @@ public final class MessagePriceItem: ListViewItem, ItemListItem { let value: Int64 let price: String public let sectionId: ItemListSectionId - let updated: (Int64) -> Void + let updated: (Int64, Bool) -> Void let openPremiumInfo: (() -> Void)? - public init(theme: PresentationTheme, strings: PresentationStrings, isEnabled: Bool, minValue: Int64, maxValue: Int64, value: Int64, price: String, sectionId: ItemListSectionId, updated: @escaping (Int64) -> Void, openPremiumInfo: (() -> Void)? = nil) { + public init(theme: PresentationTheme, strings: PresentationStrings, isEnabled: Bool, minValue: Int64, maxValue: Int64, value: Int64, price: String, sectionId: ItemListSectionId, updated: @escaping (Int64, Bool) -> Void, openPremiumInfo: (() -> Void)? = nil) { self.theme = theme self.strings = strings self.isEnabled = isEnabled @@ -259,7 +259,7 @@ private class MessagePriceItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item strongSelf.layoutParams = params - + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor @@ -342,6 +342,13 @@ private class MessagePriceItemNode: ListViewItemNode { } sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 36.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 18.0 * 2.0, height: 44.0)) + + sliderView.interactionEnded = { [weak self] in + guard let self else { + return + } + self.item?.updated(Int64(self.amount.realValue), true) + } } strongSelf.lockIconNode.isHidden = item.isEnabled @@ -436,7 +443,7 @@ private class MessagePriceItemNode: ListViewItemNode { } self.amount = updatedAmount - self.item?.updated(Int64(self.amount.realValue)) + self.item?.updated(Int64(self.amount.realValue), false) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift index 7bd70ad117..6c81f40c57 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift @@ -579,7 +579,7 @@ private class GiftIconLayer: SimpleLayer { } override func layoutSublayers() { - self.shadowLayer.frame = CGRect(origin: .zero, size: self.bounds.size).insetBy(dx: -4.0, dy: -4.0) + self.shadowLayer.frame = CGRect(origin: .zero, size: self.bounds.size).insetBy(dx: -8.0, dy: -8.0) self.animationLayer.frame = CGRect(origin: .zero, size: self.bounds.size) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index d019cf9ef8..1c0c8c45df 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -255,6 +255,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.buttonsContainerNode.clipsToBounds = true self.buttonsBackgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: true, enableSaturation: false) + self.buttonsBackgroundNode.isUserInteractionEnabled = false self.buttonsContainerNode.addSubnode(self.buttonsBackgroundNode) self.buttonsMaskView = UIView() self.buttonsBackgroundNode.view.mask = self.buttonsMaskView diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 4cd1b7c9e5..c70cee98c0 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -3728,7 +3728,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, openAgeRestrictedMessageMedia: { _, _ in }, playMessageEffect: { _ in }, editMessageFactCheck: { _ in - }, sendGift: { _ in + }, sendGift: { [weak self] _ in + guard let self else { + return + } + self.openPremiumGift() }, openUniqueGift: { _ in }, openMessageFeeException: { }, requestMessageUpdate: { _, _ in diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftContextPreviewController.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftContextPreviewController.swift index f662997902..7c1e42f704 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftContextPreviewController.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftContextPreviewController.swift @@ -120,7 +120,7 @@ private final class GiftContextPreviewComponent: Component { let subtitleSize = self.subtitle.update( transition: .immediate, component: AnyComponent(MultilineTextComponent(text: .plain( - NSAttributedString(string: "\(environment.strings.Gift_Unique_Collectible) #\(uniqueGift.number)", font: Font.regular(13.0), textColor: vibrantColor) + NSAttributedString(string: "\(environment.strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(uniqueGift.number, environment.dateTimeFormat.groupingSeparator))", font: Font.regular(13.0), textColor: vibrantColor) ))), environment: {}, containerSize: availableSize diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 12ecd981dc..e924b19dd6 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -240,6 +240,9 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } public func beginReordering() { + self.profileGifts.updateFilter(.All) + self.profileGifts.updateSorting(.date) + if let parentController = self.parentController as? PeerInfoScreen { parentController.togglePaneIsReordering(isReordering: true) } else { @@ -469,7 +472,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr return } if self.isReordering { - if !product.pinnedToTop, let reference = product.reference, let items = self.starsProducts { + if case .unique = product.gift, !product.pinnedToTop, let reference = product.reference, let items = self.starsProducts { if self.pinnedReferences.count >= self.maxPinnedCount { self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.PeerInfo_Gifts_ToastPinLimit_Text(Int32(self.maxPinnedCount)), timeout: nil, customUndoText: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) return @@ -564,7 +567,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr itemFrame = itemFrame.size.centered(around: reorderingItem.position) isReordering = true } - if itemView.layer.animation(forKey: "position") != nil && !isReordering { + if self.isReordering, itemView.layer.animation(forKey: "position") != nil && !isReordering { } else { itemTransition.setFrame(view: itemView, frame: itemFrame) } @@ -612,8 +615,6 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr var bottomScrollInset: CGFloat = 0.0 var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * (starsOptionSize.height + optionSpacing) - optionSpacing + topInset + 16.0 - let transition = ComponentTransition.immediate - let size = params.size let sideInset = params.sideInset let bottomInset = params.bottomInset @@ -681,8 +682,9 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr scrollOffset -= bottomPanelHeight } - transition.setFrame(view: panelButton.view, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - bottomInset - buttonSize.height - scrollOffset), size: buttonSize)) - transition.setAlpha(view: panelButton.view, alpha: panelAlpha) + let panelTransition = ComponentTransition.immediate + panelTransition.setFrame(view: panelButton.view, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - bottomInset - buttonSize.height - scrollOffset), size: buttonSize)) + panelTransition.setAlpha(view: panelButton.view, alpha: panelAlpha) let _ = panelButton.updateLayout(width: buttonSize.width, transition: .immediate) if self.canManage { @@ -753,16 +755,16 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.view.addSubview(panelCheckView) } panelCheckView.frame = CGRect(origin: CGPoint(x: floor((size.width - panelCheckSize.width) / 2.0), y: size.height - bottomInset - panelCheckSize.height - 11.0 - scrollOffset), size: panelCheckSize) - transition.setAlpha(view: panelCheckView, alpha: panelAlpha) + panelTransition.setAlpha(view: panelCheckView, alpha: panelAlpha) } panelButton.isHidden = true } - transition.setFrame(view: panelBackground.view, frame: CGRect(x: 0.0, y: size.height - bottomPanelHeight - scrollOffset, width: size.width, height: bottomPanelHeight)) - transition.setAlpha(view: panelBackground.view, alpha: panelAlpha) + panelTransition.setFrame(view: panelBackground.view, frame: CGRect(x: 0.0, y: size.height - bottomPanelHeight - scrollOffset, width: size.width, height: bottomPanelHeight)) + panelTransition.setAlpha(view: panelBackground.view, alpha: panelAlpha) panelBackground.update(size: CGSize(width: size.width, height: bottomPanelHeight), transition: transition.containedViewLayoutTransition) - transition.setFrame(view: panelSeparator.view, frame: CGRect(x: 0.0, y: size.height - bottomPanelHeight - scrollOffset, width: size.width, height: UIScreenPixel)) - transition.setAlpha(view: panelSeparator.view, alpha: panelAlpha) + panelTransition.setFrame(view: panelSeparator.view, frame: CGRect(x: 0.0, y: size.height - bottomPanelHeight - scrollOffset, width: size.width, height: UIScreenPixel)) + panelTransition.setAlpha(view: panelSeparator.view, alpha: panelAlpha) let fadeTransition = ComponentTransition.easeInOut(duration: 0.25) if self.resultsAreEmpty { @@ -776,8 +778,8 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.emptyResultsClippingView.isHidden = false - transition.setFrame(view: self.emptyResultsClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: 48.0), size: self.scrollNode.frame.size)) - transition.setBounds(view: self.emptyResultsClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: 48.0), size: self.scrollNode.frame.size)) + panelTransition.setFrame(view: self.emptyResultsClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: 48.0), size: self.scrollNode.frame.size)) + panelTransition.setBounds(view: self.emptyResultsClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: 48.0), size: self.scrollNode.frame.size)) let emptyResultsTitleSize = self.emptyResultsTitle.update( transition: .immediate, @@ -840,7 +842,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr view.playOnce() } view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size) - transition.setPosition(view: view, position: emptyResultsAnimationFrame.center) + panelTransition.setPosition(view: view, position: emptyResultsAnimationFrame.center) } if let view = self.emptyResultsTitle.view { if view.superview == nil { @@ -849,7 +851,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.emptyResultsClippingView.addSubview(view) } view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size) - transition.setPosition(view: view, position: emptyResultsTitleFrame.center) + panelTransition.setPosition(view: view, position: emptyResultsTitleFrame.center) } if let view = self.emptyResultsAction.view { if view.superview == nil { @@ -858,7 +860,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.emptyResultsClippingView.addSubview(view) } view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size) - transition.setPosition(view: view, position: emptyResultsActionFrame.center) + panelTransition.setPosition(view: view, position: emptyResultsActionFrame.center) } } else { if let view = self.emptyResultsAnimation.view { @@ -879,7 +881,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } } - if self.peerId == self.context.account.peerId { + if self.peerId == self.context.account.peerId, !self.resultsAreEmpty { let footerText: ComponentView if let current = self.footerText { footerText = current @@ -941,21 +943,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.chatControllerInteraction.navigationController()?.pushViewController(controller) }) } else { - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Birthday(id: self.peerId)) - |> deliverOnMainQueue).start(next: { birthday in - var hasBirthday = false - if let birthday { - hasBirthday = hasBirthdayToday(birthday: birthday) - } - let controller = self.context.sharedContext.makeGiftOptionsController( - context: self.context, - peerId: self.peerId, - premiumOptions: [], - hasBirthday: hasBirthday, - completion: nil - ) - self.chatControllerInteraction.navigationController()?.pushViewController(controller) - }) + self.chatControllerInteraction.sendGift(self.peerId) } } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index 39de43b8ec..b4066cab07 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -424,7 +424,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { isPaidMessage = true titleText = strings.Stars_Transaction_PaidMessage(transaction.paidMessageCount ?? 1) countOnTop = true - descriptionText = strings.Stars_Transaction_PaidMessage_Text(formatPermille(starrefCommissionPermille)).string + descriptionText = strings.Stars_Transaction_PaidMessage_Text(formatPermille(1000 - starrefCommissionPermille)).string } else if transaction.starrefPeerId == nil { titleText = strings.StarsTransaction_TitleCommission(formatPermille(starrefCommissionPermille)).string countOnTop = false diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index b5f9e00149..f9aacdbe72 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -312,7 +312,7 @@ final class StarsTransactionsListPanelComponent: Component { itemSubtitle = environment.strings.Stars_Intro_Transaction_PaidMessage(item.paidMessageCount ?? 1) } else if let starGift = item.starGift { if item.flags.contains(.isStarGiftUpgrade), case let .unique(gift) = starGift { - itemTitle = "\(gift.title) #\(gift.number)" + itemTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, environment.dateTimeFormat.groupingSeparator))" itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftUpgrade uniqueGift = gift } else { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 95e775a17a..753e8f3c60 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2905,7 +2905,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { Queue.mainQueue().after(0.3) { let tooltipController = UndoOverlayController( presentationData: presentationData, - content: .forward(savedMessages: false, text: presentationData.strings.Gift_Transfer_Success("\(gift.title) #\(gift.number)", peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string), + content: .forward(savedMessages: false, text: presentationData.strings.Gift_Transfer_Success("\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))", peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string), elevatedLayout: false, action: { _ in return true } ) diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index f1be891cc9..ad3ab346ab 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -65,10 +65,10 @@ function tgBrowserHandleMutations(mutations) { if (mutation.addedNodes && mutation.addedNodes.length > 0) { mutation.addedNodes.forEach((newNode) => { if (newNode.tagName === 'VIDEO') { - disableWebkitEnterFullscreen(newNode); + tgBrowserDisableWebkitEnterFullscreen(newNode); } if (newNode.querySelectorAll) { - newNode.querySelectorAll('video').forEach(disableWebkitEnterFullscreen); + newNode.querySelectorAll('video').forEach(tgBrowserDisableWebkitEnterFullscreen); } }); } diff --git a/versions.json b/versions.json index ded113cd87..26fd2ba0c2 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "11.8", + "app": "11.8.1", "xcode": "16.2", "bazel": "7.3.1:981f82a470bad1349322b6f51c9c6ffa0aa291dab1014fac411543c12e661dff", "macos": "15" From c39ba6925022b5df7ace79c9ba3a39d868e2cca2 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 7 Mar 2025 23:18:38 +0400 Subject: [PATCH 02/19] Fix filtered gifts pinning --- .../Sources/TelegramEngine/Payments/StarGifts.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 72c1af1dc3..a197b0609c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -1210,6 +1210,10 @@ private final class ProfileGiftsContextImpl { if let reference = gift.reference, existingGifts.contains(reference) { continue } + var gift = gift + if gift.reference == reference { + gift = gift.withPinnedToTop(pinnedToTop) + } updatedGifts.append(gift) } updatedGifts.sort { lhs, rhs in @@ -1238,6 +1242,10 @@ private final class ProfileGiftsContextImpl { if let reference = gift.reference, existingFilteredGifts.contains(reference) { continue } + var gift = gift + if gift.reference == reference { + gift = gift.withPinnedToTop(pinnedToTop) + } updatedFilteredGifts.append(gift) } updatedFilteredGifts.sort { lhs, rhs in From a9ce88255dd69ee514ddea48317cfcb6ec03906c Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 8 Mar 2025 00:01:18 +0400 Subject: [PATCH 03/19] Various fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 4 + .../ContactMultiselectionController.swift | 5 +- .../IncomingMessagePrivacyScreen.swift | 7 +- ...ectivePrivacySettingsPeersController.swift | 5 +- .../Sources/StarsStatisticsScreen.swift | 184 ++++++++++++++---- .../Sources/StarsTransactionsScreen.swift | 110 +++++------ .../Stars/Stats.imageset/Contents.json | 12 ++ .../Premium/Stars/Stats.imageset/stats_24.pdf | Bin 0 -> 3775 bytes .../ContactMultiselectionControllerNode.swift | 3 + 9 files changed, 224 insertions(+), 106 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/Stats.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/Stats.imageset/stats_24.pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 3df82e28b0..272447cd7e 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13941,6 +13941,7 @@ Sorry for the inconvenience."; "Stars.Intro.BuyShort" = "Top Up"; "Stars.Intro.Withdraw" = "Withdraw"; +"Stars.Intro.Stats" = "Stats"; "Group.Info.Settings" = "Group Settings"; @@ -13992,3 +13993,6 @@ Sorry for the inconvenience."; "Conversation.VideoTimeLinkCopied" = "Link with start time at %@ copied to clipboard."; "Share.VideoStartAt" = "Start at %@"; "SendStarReactions.SubtitleFrom" = "from %@"; + +"Stars.AccountRevenue.Proceeds.Info" = "Stars from your total balance can be withdrawn as rewards 21 days after they are earned."; +"Stars.AccountRevenue.Withdraw.Info" = "You can collect rewards for Stars using Fragment. You cannot withdraw less than 1000 stars. [Learn More >]()"; diff --git a/submodules/AccountContext/Sources/ContactMultiselectionController.swift b/submodules/AccountContext/Sources/ContactMultiselectionController.swift index f790e0ba78..fc3ea9691c 100644 --- a/submodules/AccountContext/Sources/ContactMultiselectionController.swift +++ b/submodules/AccountContext/Sources/ContactMultiselectionController.swift @@ -48,6 +48,7 @@ public enum ContactMultiselectionControllerMode { public var onlyUsers: Bool public var disableChannels: Bool public var disableBots: Bool + public var disableContacts: Bool public init( title: String, @@ -59,7 +60,8 @@ public enum ContactMultiselectionControllerMode { displayPresence: Bool = false, onlyUsers: Bool = false, disableChannels: Bool = false, - disableBots: Bool = false + disableBots: Bool = false, + disableContacts: Bool = false ) { self.title = title self.searchPlaceholder = searchPlaceholder @@ -71,6 +73,7 @@ public enum ContactMultiselectionControllerMode { self.onlyUsers = onlyUsers self.disableChannels = disableChannels self.disableBots = disableBots + self.disableContacts = disableContacts } } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift index 090e6672ed..4f5ddfc578 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift @@ -281,8 +281,9 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP chatListFilters: nil, onlyUsers: false, disableChannels: true, - disableBots: false - )), filters: [.excludeSelf])) + disableBots: true, + disableContacts: true + )))) addPeerDisposable.set((controller.result |> take(1) |> deliverOnMainQueue).start(next: { [weak controller] result in @@ -342,7 +343,7 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP controller.navigationPresentation = .modal pushControllerImpl?(controller) } else { - let controller = selectivePrivacyPeersController(context: context, title: presentationData.strings.Privacy_Messages_Exceptions_Title, footer: presentationData.strings.Privacy_Messages_RemoveFeeInfo, initialPeers: peerIds, initialEnableForPremium: false, displayPremiumCategory: false, initialEnableForBots: false, displayBotsCategory: false, updated: { updatedPeerIds, _, _ in + let controller = selectivePrivacyPeersController(context: context, title: presentationData.strings.Privacy_Messages_Exceptions_Title, footer: presentationData.strings.Privacy_Messages_RemoveFeeInfo, hideContacts: true, initialPeers: peerIds, initialEnableForPremium: false, displayPremiumCategory: false, initialEnableForBots: false, displayBotsCategory: false, updated: { updatedPeerIds, _, _ in updateState { state in var updatedState = state updatedState.disableFor = updatedPeerIds diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift index 9d79acf509..e18a0ac730 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift @@ -322,7 +322,7 @@ private func selectivePrivacyPeersControllerEntries(presentationData: Presentati return entries } -public func selectivePrivacyPeersController(context: AccountContext, title: String, footer: String? = nil, initialPeers: [EnginePeer.Id: SelectivePrivacyPeer], initialEnableForPremium: Bool, displayPremiumCategory: Bool, initialEnableForBots: Bool, displayBotsCategory: Bool, updated: @escaping ([EnginePeer.Id: SelectivePrivacyPeer], Bool, Bool) -> Void) -> ViewController { +public func selectivePrivacyPeersController(context: AccountContext, title: String, footer: String? = nil, hideContacts: Bool = false, initialPeers: [EnginePeer.Id: SelectivePrivacyPeer], initialEnableForPremium: Bool, displayPremiumCategory: Bool, initialEnableForBots: Bool, displayBotsCategory: Bool, updated: @escaping ([EnginePeer.Id: SelectivePrivacyPeer], Bool, Bool) -> Void) -> ViewController { let initialState = SelectivePrivacyPeersControllerState(enableForPremium: initialEnableForPremium, enableForBots: initialEnableForBots, editing: false, peerIdWithRevealedOptions: nil) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) @@ -428,7 +428,8 @@ public func selectivePrivacyPeersController(context: AccountContext, title: Stri chatListFilters: nil, onlyUsers: false, disableChannels: true, - disableBots: false + disableBots: hideContacts, + disableContacts: hideContacts )), alwaysEnabled: true)) addPeerDisposable.set((controller.result |> take(1) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift index ea9e2ce6c5..626f027cb0 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift @@ -29,6 +29,7 @@ final class StarsStatisticsScreenComponent: Component { let peerId: EnginePeer.Id let revenueContext: StarsRevenueStatsContext let openTransaction: (StarsContext.State.Transaction) -> Void + let buy: () -> Void let withdraw: () -> Void let showTimeoutTooltip: (Int32) -> Void let buyAds: () -> Void @@ -38,6 +39,7 @@ final class StarsStatisticsScreenComponent: Component { peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext, openTransaction: @escaping (StarsContext.State.Transaction) -> Void, + buy: @escaping () -> Void, withdraw: @escaping () -> Void, showTimeoutTooltip: @escaping (Int32) -> Void, buyAds: @escaping () -> Void @@ -46,6 +48,7 @@ final class StarsStatisticsScreenComponent: Component { self.peerId = peerId self.revenueContext = revenueContext self.openTransaction = openTransaction + self.buy = buy self.withdraw = withdraw self.showTimeoutTooltip = showTimeoutTooltip self.buyAds = buyAds @@ -508,7 +511,7 @@ final class StarsStatisticsScreenComponent: Component { )), footer: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: strings.Stars_BotRevenue_Proceeds_Info, + string: component.peerId == component.context.account.peerId ? strings.Stars_AccountRevenue_Proceeds_Info : strings.Stars_BotRevenue_Proceeds_Info, font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor )), @@ -558,8 +561,8 @@ final class StarsStatisticsScreenComponent: Component { return (TelegramTextAttributes.URL, contents) }) - let balanceInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(strings.Stars_BotRevenue_Withdraw_Info, attributes: termsMarkdownAttributes, textAlignment: .natural - )) + let balanceRawString = component.peerId == component.context.account.peerId ? strings.Stars_AccountRevenue_Withdraw_Info : strings.Stars_BotRevenue_Withdraw_Info + let balanceInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(balanceRawString, attributes: termsMarkdownAttributes, textAlignment: .natural)) if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme { self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) } @@ -567,6 +570,96 @@ final class StarsStatisticsScreenComponent: Component { balanceInfoString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: balanceInfoString.string)) } + var balanceItems: [AnyComponentWithIdentity] = [] + if component.peerId == component.context.account.peerId { + let withdrawEnabled = self.starsState?.balances.withdrawEnabled ?? false + balanceItems = [ + AnyComponentWithIdentity(id: 0, component: AnyComponent( + StarsBalanceComponent( + theme: environment.theme, + strings: strings, + dateTimeFormat: environment.dateTimeFormat, + count: self.starsState?.balances.availableBalance ?? StarsAmount.zero, + rate: self.starsState?.usdRate ?? 0, + actionTitle: strings.Stars_Intro_BuyShort, + actionAvailable: true, + actionIsEnabled: true, + actionIcon: PresentationResourcesItemList.itemListRoundTopupIcon(environment.theme), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.buy() + }, + secondaryActionTitle: withdrawEnabled ? strings.Stars_Intro_Withdraw : nil, + secondaryActionIcon: PresentationResourcesItemList.itemListRoundWithdrawIcon(environment.theme), + secondaryActionCooldownUntilTimestamp: self.starsState?.balances.nextWithdrawalTimestamp, + secondaryAction: { [weak self] in + guard let self, let component = self.component else { + return + } + var remainingCooldownSeconds: Int32 = 0 + if let cooldownUntilTimestamp = self.starsState?.balances.nextWithdrawalTimestamp { + remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) + remainingCooldownSeconds = max(0, remainingCooldownSeconds) + + if remainingCooldownSeconds > 0 { + component.showTimeoutTooltip(cooldownUntilTimestamp) + } else { + component.withdraw() + } + } else { + component.withdraw() + } + } + ) + )) + ] + } else { + balanceItems = [ + AnyComponentWithIdentity(id: 0, component: AnyComponent( + StarsBalanceComponent( + theme: environment.theme, + strings: strings, + dateTimeFormat: environment.dateTimeFormat, + count: self.starsState?.balances.availableBalance ?? StarsAmount.zero, + rate: self.starsState?.usdRate ?? 0, + actionTitle: strings.Stars_BotRevenue_Withdraw_WithdrawShort, + actionAvailable: true, + actionIsEnabled: self.starsState?.balances.withdrawEnabled ?? true, + actionCooldownUntilTimestamp: self.starsState?.balances.nextWithdrawalTimestamp, + actionIcon: PresentationResourcesItemList.itemListRoundTopupIcon(environment.theme), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + var remainingCooldownSeconds: Int32 = 0 + if let cooldownUntilTimestamp = self.starsState?.balances.nextWithdrawalTimestamp { + remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) + remainingCooldownSeconds = max(0, remainingCooldownSeconds) + + if remainingCooldownSeconds > 0 { + component.showTimeoutTooltip(cooldownUntilTimestamp) + } else { + component.withdraw() + } + } else { + component.withdraw() + } + }, + secondaryActionTitle: strings.Stars_BotRevenue_Withdraw_BuyAds, + secondaryActionIcon: PresentationResourcesItemList.itemListRoundWithdrawIcon(environment.theme), + secondaryAction: { [weak self] in + guard let self, let component = self.component else { + return + } + component.buyAds() + } + ) + )) + ] + } + let balanceSize = self.balanceView.update( transition: .immediate, component: AnyComponent(ListSectionComponent( @@ -597,44 +690,7 @@ final class StarsStatisticsScreenComponent: Component { } } )), - items: [AnyComponentWithIdentity(id: 0, component: AnyComponent( - StarsBalanceComponent( - theme: environment.theme, - strings: strings, - dateTimeFormat: environment.dateTimeFormat, - count: self.starsState?.balances.availableBalance ?? StarsAmount.zero, - rate: self.starsState?.usdRate ?? 0, - actionTitle: strings.Stars_BotRevenue_Withdraw_WithdrawShort, - actionAvailable: true, - actionIsEnabled: self.starsState?.balances.withdrawEnabled ?? true, - actionCooldownUntilTimestamp: self.starsState?.balances.nextWithdrawalTimestamp, - action: { [weak self] in - guard let self, let component = self.component else { - return - } - var remainingCooldownSeconds: Int32 = 0 - if let cooldownUntilTimestamp = self.starsState?.balances.nextWithdrawalTimestamp { - remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) - remainingCooldownSeconds = max(0, remainingCooldownSeconds) - - if remainingCooldownSeconds > 0 { - component.showTimeoutTooltip(cooldownUntilTimestamp) - } else { - component.withdraw() - } - } else { - component.withdraw() - } - }, - secondaryActionTitle: strings.Stars_BotRevenue_Withdraw_BuyAds, - secondaryAction: { [weak self] in - guard let self, let component = self.component else { - return - } - component.buyAds() - } - ) - ))] + items: balanceItems )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInsets, height: availableSize.height) @@ -799,11 +855,14 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer { private weak var tooltipScreen: UndoOverlayController? private var timer: Foundation.Timer? + private let options = Promise<[StarsTopUpOption]>() + public init(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) { self.context = context self.peerId = peerId self.revenueContext = revenueContext + var buyImpl: (() -> Void)? var withdrawImpl: (() -> Void)? var buyAdsImpl: (() -> Void)? var showTimeoutTooltipImpl: ((Int32) -> Void)? @@ -815,6 +874,9 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer { openTransaction: { transaction in openTransactionImpl?(transaction) }, + buy: { + buyImpl?() + }, withdraw: { withdrawImpl?() }, @@ -842,6 +904,46 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer { }) } + if peerId == context.account.peerId { + self.options.set(.single([]) |> then(context.engine.payments.starsTopUpOptions())) + } + + buyImpl = { [weak self] in + guard let self else { + return + } + let _ = (self.options.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] options in + guard let self, let starsContext = context.starsContext else { + return + } + let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: .generic, completion: { [weak self] stars in + guard let self else { + return + } + starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .universal( + animation: "StarsBuy", + scale: 0.066, + colors: [:], + title: presentationData.strings.Stars_Intro_PurchasedTitle, + text: presentationData.strings.Stars_Intro_PurchasedText(presentationData.strings.Stars_Intro_PurchasedText_Stars(Int32(stars))).string, + customUndoText: nil, + timeout: nil + ), + elevatedLayout: false, + action: { _ in return true}) + self.present(resultController, in: .window(.root)) + }) + self.push(controller) + }) + } + withdrawImpl = { [weak self] in guard let self else { return diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 6894f31d53..7bfd83869e 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -630,7 +630,7 @@ final class StarsTransactionsScreenComponent: Component { contentHeight += descriptionSize.height contentHeight += 29.0 - let withdrawAvailable = self.revenueState?.balances.withdrawEnabled ?? false + let withdrawAvailable = (self.revenueState?.balances.overallRevenue.value ?? 0) > 0 let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) let balanceSize = self.balanceView.update( @@ -656,26 +656,14 @@ final class StarsTransactionsScreenComponent: Component { } component.buy() }, - secondaryActionTitle: withdrawAvailable ? environment.strings.Stars_Intro_Withdraw : nil, - secondaryActionIcon: withdrawAvailable ? PresentationResourcesItemList.itemListRoundWithdrawIcon(environment.theme) : nil, + secondaryActionTitle: withdrawAvailable ? environment.strings.Stars_Intro_Stats : nil, + secondaryActionIcon: withdrawAvailable ? PresentationResourcesItemList.itemListStatsIcon(environment.theme) : nil, secondaryActionCooldownUntilTimestamp: self.revenueState?.balances.nextWithdrawalTimestamp, secondaryAction: withdrawAvailable ? { [weak self] in guard let self, let component = self.component else { return } - var remainingCooldownSeconds: Int32 = 0 - if let cooldownUntilTimestamp = self.revenueState?.balances.nextWithdrawalTimestamp { - remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) - remainingCooldownSeconds = max(0, remainingCooldownSeconds) - - if remainingCooldownSeconds > 0 { - component.showTimeoutTooltip(cooldownUntilTimestamp) - } else { - component.withdraw() - } - } else { - component.withdraw() - } + component.withdraw() } : nil, additionalAction: (premiumConfiguration.starsGiftsPurchaseAvailable && !premiumConfiguration.isPremiumDisabled) ? AnyComponent( Button( @@ -739,7 +727,7 @@ final class StarsTransactionsScreenComponent: Component { return } let _ = (component.context.sharedContext.makeAffiliateProgramSetupScreenInitialData(context: component.context, peerId: component.context.account.peerId, mode: .connectedPrograms) - |> deliverOnMainQueue).startStandalone(next: { [weak self] initialData in + |> deliverOnMainQueue).startStandalone(next: { [weak self] initialData in guard let self, let component = self.component else { return } @@ -1236,48 +1224,52 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { guard let self else { return } - let _ = (context.engine.peers.checkStarsRevenueWithdrawalAvailability() - |> deliverOnMainQueue).start(error: { [weak self] error in - guard let self else { - return - } - switch error { - case .serverProvided: - return - case .requestPassword: - let _ = (self.starsRevenueStatsContext.state - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] state in - guard let self else { - return - } - let controller = self.context.sharedContext.makeStarsWithdrawalScreen(context: context, completion: { [weak self] amount in - guard let self else { - return - } - let controller = confirmStarsRevenueWithdrawalController(context: context, peerId: context.account.peerId, amount: amount, present: { [weak self] c, a in - self?.present(c, in: .window(.root)) - }, completion: { [weak self] 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) { - self?.starsRevenueStatsContext.reload() - } - }) - self.present(controller, in: .window(.root)) - }) - self.push(controller) - }) - default: - let controller = starsRevenueWithdrawalController(context: context, peerId: context.account.peerId, amount: 0, initialError: error, present: { [weak self] c, a in - self?.present(c, in: .window(.root)) - }, completion: { _ in - - }) - self.present(controller, in: .window(.root)) - } - }) + + let controller = self.context.sharedContext.makeStarsStatisticsScreen(context: context, peerId: context.account.peerId, revenueContext: self.starsRevenueStatsContext) + self.push(controller) + +// let _ = (context.engine.peers.checkStarsRevenueWithdrawalAvailability() +// |> deliverOnMainQueue).start(error: { [weak self] error in +// guard let self else { +// return +// } +// switch error { +// case .serverProvided: +// return +// case .requestPassword: +// let _ = (self.starsRevenueStatsContext.state +// |> take(1) +// |> deliverOnMainQueue).start(next: { [weak self] state in +// guard let self else { +// return +// } +// let controller = self.context.sharedContext.makeStarsWithdrawalScreen(context: context, completion: { [weak self] amount in +// guard let self else { +// return +// } +// let controller = confirmStarsRevenueWithdrawalController(context: context, peerId: context.account.peerId, amount: amount, present: { [weak self] c, a in +// self?.present(c, in: .window(.root)) +// }, completion: { [weak self] 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) { +// self?.starsRevenueStatsContext.reload() +// } +// }) +// self.present(controller, in: .window(.root)) +// }) +// self.push(controller) +// }) +// default: +// let controller = starsRevenueWithdrawalController(context: context, peerId: context.account.peerId, amount: 0, initialError: error, present: { [weak self] c, a in +// self?.present(c, in: .window(.root)) +// }, completion: { _ in +// +// }) +// self.present(controller, in: .window(.root)) +// } +// }) } showTimeoutTooltipImpl = { [weak self] cooldownUntilTimestamp in diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/Stats.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Stats.imageset/Contents.json new file mode 100644 index 0000000000..a243b957ee --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Stats.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "stats_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/Stats.imageset/stats_24.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Stats.imageset/stats_24.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c054ff1aa737d2f9b26cca187bfdf095fa86a060 GIT binary patch literal 3775 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 Date: Sat, 8 Mar 2025 00:12:08 +0400 Subject: [PATCH 04/19] Fix --- .../StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift index 626f027cb0..f7fcdc9a88 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift @@ -628,7 +628,6 @@ final class StarsStatisticsScreenComponent: Component { actionAvailable: true, actionIsEnabled: self.starsState?.balances.withdrawEnabled ?? true, actionCooldownUntilTimestamp: self.starsState?.balances.nextWithdrawalTimestamp, - actionIcon: PresentationResourcesItemList.itemListRoundTopupIcon(environment.theme), action: { [weak self] in guard let self, let component = self.component else { return @@ -648,7 +647,6 @@ final class StarsStatisticsScreenComponent: Component { } }, secondaryActionTitle: strings.Stars_BotRevenue_Withdraw_BuyAds, - secondaryActionIcon: PresentationResourcesItemList.itemListRoundWithdrawIcon(environment.theme), secondaryAction: { [weak self] in guard let self, let component = self.component else { return From a6708979ba310c8b801c05c09136d30e6a9f4a0c Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 8 Mar 2025 00:15:34 +0400 Subject: [PATCH 05/19] Fix --- .../Sources/StarsTransactionsScreen.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 7bfd83869e..d3656812ce 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -658,7 +658,6 @@ final class StarsTransactionsScreenComponent: Component { }, secondaryActionTitle: withdrawAvailable ? environment.strings.Stars_Intro_Stats : nil, secondaryActionIcon: withdrawAvailable ? PresentationResourcesItemList.itemListStatsIcon(environment.theme) : nil, - secondaryActionCooldownUntilTimestamp: self.revenueState?.balances.nextWithdrawalTimestamp, secondaryAction: withdrawAvailable ? { [weak self] in guard let self, let component = self.component else { return From 8ddd793a36f4955005209a6699b2f9e02ac243f1 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 8 Mar 2025 00:17:11 +0400 Subject: [PATCH 06/19] Fix build --- .../Sources/Resources/PresentationResourceKey.swift | 1 + .../Sources/Resources/PresentationResourcesItemList.swift | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 2e719e4694..60233f2ca0 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -79,6 +79,7 @@ public enum PresentationResourceKey: Int32 { case itemListPremiumIcon case itemListRoundTopupIcon case itemListRoundWithdrawIcon + case itemListStatsIcon case statsReactionsIcon case statsForwardsIcon diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift index 73f170cecf..677ba5637c 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift @@ -465,4 +465,10 @@ public struct PresentationResourcesItemList { }) }) } + + public static func itemListStatsIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.itemListStatsIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Premium/Stars/Stats"), color: .white)?.withRenderingMode(.alwaysTemplate) + }) + } } From 15c181bc52e802c925f0b24ff4ea9d6f32d5c886 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 8 Mar 2025 00:47:07 +0400 Subject: [PATCH 07/19] Fix paid message resend --- .../PendingMessages/EnqueueMessage.swift | 22 ++++++++++++++++++- .../Sources/ChatUserInfoItem.swift | 6 ++--- .../Chat/ChatControllerPaidMessage.swift | 6 +++-- .../TelegramUI/Sources/ChatController.swift | 22 ++++++++++++++----- 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index 8c66c43bae..8758b43e1c 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -404,6 +404,19 @@ public func resendMessages(account: Account, messageIds: [MessageId]) -> Signal< return account.postbox.transaction { transaction -> Void in var removeMessageIds: [MessageId] = [] for (peerId, ids) in messagesIdsGroupedByPeerId(messageIds) { + var sendPaidMessageStars: StarsAmount? + let peer = transaction.getPeer(peerId) + if let user = peer as? TelegramUser, user.flags.contains(.requireStars) { + if let cachedUserData = transaction.getPeerCachedData(peerId: user.id) as? CachedUserData { + sendPaidMessageStars = cachedUserData.sendPaidMessageStars + } + } else if let channel = peer as? TelegramChannel { + if channel.flags.contains(.isCreator) || channel.adminRights != nil { + } else { + sendPaidMessageStars = channel.sendPaidMessageStars + } + } + var messages: [EnqueueMessage] = [] for id in ids { if let message = transaction.getMessage(id), !message.flags.contains(.Incoming) { @@ -425,9 +438,16 @@ public func resendMessages(account: Account, messageIds: [MessageId]) -> Signal< } else if let attribute = attribute as? ForwardSourceInfoAttribute { forwardSource = attribute.messageId } else { - filteredAttributes.append(attribute) + if attribute is PaidStarsMessageAttribute { + } else { + filteredAttributes.append(attribute) + } } } + + if let sendPaidMessageStars { + filteredAttributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: false)) + } if let forwardSource = forwardSource { messages.append(.forward(source: forwardSource, threadId: nil, grouping: .auto, attributes: filteredAttributes, correlationId: nil)) diff --git a/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift index b20939179e..9bf11f830f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift @@ -311,10 +311,10 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe } else { var countryName = "" let countriesConfiguration = item.context.currentCountriesConfiguration.with { $0 } - if let country = countriesConfiguration.countries.first(where: { $0.id == phoneCountry }) { - countryName = country.localizedName ?? country.name - } else if phoneCountry == "FT" { + if phoneCountry == "FT" { countryName = item.presentationData.strings.Chat_NonContactUser_AnonymousNumber + } else if let country = countriesConfiguration.countries.first(where: { $0.id == phoneCountry }) { + countryName = country.localizedName ?? country.name } else if phoneCountry == "TS" { countryName = "Test" } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift index ae7516293a..cae3d5b3d9 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift @@ -15,7 +15,7 @@ import TelegramPresentationData import TelegramNotices extension ChatControllerImpl { - func presentPaidMessageAlertIfNeeded(count: Int32 = 1, forceDark: Bool = false, completion: @escaping (Bool) -> Void) { + func presentPaidMessageAlertIfNeeded(count: Int32 = 1, forceDark: Bool = false, alwaysAsk: Bool = false, completion: @escaping (Bool) -> Void) { guard let peer = self.presentationInterfaceState.renderedPeer?.peer.flatMap(EnginePeer.init) else { completion(false) return @@ -24,11 +24,12 @@ extension ChatControllerImpl { let totalAmount = sendPaidMessageStars.value * Int64(count) let _ = (ApplicationSpecificNotice.dismissedPaidMessageWarningNamespace(accountManager: self.context.sharedContext.accountManager, peerId: peer.id) + |> take(1) |> deliverOnMainQueue).start(next: { [weak self] dismissedAmount in guard let self, let starsContext = self.context.starsContext else { return } - if let dismissedAmount, dismissedAmount == sendPaidMessageStars.value, let currentState = starsContext.currentState, currentState.balance.value > totalAmount { + if !alwaysAsk, let dismissedAmount, dismissedAmount == sendPaidMessageStars.value, let currentState = starsContext.currentState, currentState.balance.value > totalAmount { if count < 3 && totalAmount < 100 { completion(false) } else { @@ -52,6 +53,7 @@ extension ChatControllerImpl { count: count, amount: sendPaidMessageStars, totalAmount: nil, + hasCheck: !alwaysAsk, navigationController: self.navigationController as? NavigationController, completion: { [weak self] dontAskAgain in guard let self else { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index fda440a9a1..f61918c4e7 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -3140,19 +3140,29 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G actions.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_MessageDialogRetry, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in - if let strongSelf = self { - let _ = resendMessages(account: strongSelf.context.account, messageIds: selectedGroup.map({ $0.id })).startStandalone() + if let self { + self.presentPaidMessageAlertIfNeeded(count: Int32(selectedGroup.count), alwaysAsk: true, completion: { [weak self] _ in + guard let self else { + return + } + let _ = resendMessages(account: self.context.account, messageIds: selectedGroup.map({ $0.id })).startStandalone() + }) + f(self.presentationInterfaceState.sendPaidMessageStars == nil ? .dismissWithoutContent : .default) } - f(.dismissWithoutContent) }))) if totalGroupCount != 1 { actions.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_MessageDialogRetryAll(totalGroupCount).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in - if let strongSelf = self { - let _ = resendMessages(account: strongSelf.context.account, messageIds: messages.map({ $0.id })).startStandalone() + if let self { + self.presentPaidMessageAlertIfNeeded(count: Int32(messages.count), alwaysAsk: true, completion: { [weak self] _ in + guard let self else { + return + } + let _ = resendMessages(account: self.context.account, messageIds: messages.map({ $0.id })).startStandalone() + }) + f(self.presentationInterfaceState.sendPaidMessageStars == nil ? .dismissWithoutContent : .default) } - f(.dismissWithoutContent) }))) } actions.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in From f619e628f1b1fd02702b6d737dd1d19c716b50ef Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 8 Mar 2025 00:56:28 +0400 Subject: [PATCH 08/19] Fix --- .../Gifts/GiftViewScreen/Sources/GiftViewScreen.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 9d2c630241..1ca31db8c7 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -2638,7 +2638,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { giftsPeerId = peerId text = added ? presentationData.strings.Gift_Displayed_ChannelText : presentationData.strings.Gift_Hidden_ChannelText } else { - giftsPeerId = arguments.peerId + giftsPeerId = context.account.peerId text = added ? presentationData.strings.Gift_Displayed_NewText : presentationData.strings.Gift_Hidden_NewText } From 0f8def4348e66864bbfb3a0f4fe194ae9fda4cf1 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 8 Mar 2025 03:10:12 +0400 Subject: [PATCH 09/19] Various fixes --- .../PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift | 4 ++++ .../TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index c70cee98c0..50025887cf 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -12707,6 +12707,10 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc self.starsContext = nil } + if isMyProfile, let profileGiftsContext { + profileGiftsContext.reload() + } + self.presentationData = updatedPresentationData?.0 ?? context.sharedContext.currentPresentationData.with { $0 } let baseNavigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData) diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index e5ba6f0de2..7844f12364 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -620,7 +620,7 @@ extension ChatControllerImpl { if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer, let starsContext = context.starsContext { let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions if !premiumGiftOptions.isEmpty { - let controller = PremiumGiftAttachmentScreen(context: context, starsContext: starsContext, peerId: peer.id, premiumOptions: premiumGiftOptions, hasBirthday: true, completion: { [weak self] in + let controller = PremiumGiftAttachmentScreen(context: context, starsContext: starsContext, peerId: peer.id, premiumOptions: premiumGiftOptions, hasBirthday: strongSelf.presentationInterfaceState.hasBirthdayToday, completion: { [weak self] in guard let self else { return } From b864b88935da4a6dc9b5dbe18454236d993d8eb6 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 8 Mar 2025 03:11:26 +0400 Subject: [PATCH 10/19] Various fixes --- .../PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 50025887cf..1673cf93da 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -12708,6 +12708,8 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc } if isMyProfile, let profileGiftsContext { + profileGiftsContext.updateFilter(.All) + profileGiftsContext.updateSorting(.date) profileGiftsContext.reload() } From 2ecbaf4507f51b1d0e0a276f5bebf8a8c9b31583 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 10 Mar 2025 15:32:15 +0400 Subject: [PATCH 11/19] Various improvements --- .../Sources/AccountContext.swift | 2 + .../AccountContext/Sources/Premium.swift | 38 ++ submodules/AuthorizationUI/BUILD | 1 + .../AuthorizationSequenceController.swift | 24 + .../AuthorizationSequencePaymentScreen.swift | 287 +++++++++ .../Sources/ChatListController.swift | 8 + .../Sources/ChatListControllerNode.swift | 4 + .../Sources/ChatListSearchListPaneNode.swift | 2 + .../Sources/ChatListShimmerNode.swift | 1 + .../Sources/Node/ChatListNode.swift | 35 +- .../Sources/Node/ChatListNodeEntries.swift | 1 + .../Sources/Node/ChatListNoticeItem.swift | 4 + .../Sources/ViewControllerComponent.swift | 68 ++- .../Sources/ContactListNode.swift | 2 +- .../Sources/InAppPurchaseManager.swift | 109 +++- .../Sources/MTRequestMessageService.m | 2 +- .../TextSizeSelectionController.swift | 1 + .../Themes/ThemePreviewControllerNode.swift | 1 + submodules/TelegramApi/Sources/Api0.swift | 10 +- submodules/TelegramApi/Sources/Api1.swift | 20 +- submodules/TelegramApi/Sources/Api13.swift | 36 ++ submodules/TelegramApi/Sources/Api26.swift | 22 + submodules/TelegramApi/Sources/Api29.swift | 20 + submodules/TelegramApi/Sources/Api3.swift | 36 ++ submodules/TelegramApi/Sources/Api38.swift | 13 +- submodules/TelegramApi/Sources/Api5.swift | 20 +- .../TelegramCore/Sources/Authorization.swift | 17 + .../Sources/State/Serialization.swift | 2 +- .../SyncCore_UnauthorizedAccountState.swift | 16 + .../ChangeAccountPhoneNumber.swift | 4 +- .../Auth/CancelAccountReset.swift | 4 +- .../ItemCache/TelegramEngineItemCache.swift | 41 ++ .../Messages/QuickReplyMessages.swift | 2 +- .../TelegramEngine/Payments/AppStore.swift | 33 +- .../TelegramEngine/Payments/Stars.swift | 27 + .../Payments/TelegramEnginePayments.swift | 22 +- .../Peers/UpdateCachedPeerData.swift | 2 +- .../SecureId/VerifySecureIdValue.swift | 2 +- .../TelegramEngine/TelegramEngine.swift | 8 + .../Sources/TonFormat.swift | 2 +- submodules/TelegramUI/BUILD | 1 + .../AdsInfoScreen/Sources/AdsInfoScreen.swift | 1 + .../ChatEmptyNode/Sources/ChatEmptyNode.swift | 4 +- ...ChatInlineSearchResultsListComponent.swift | 2 + .../Sources/ChatMessageBubbleItemNode.swift | 6 +- .../Sources/ChatUserInfoItem.swift | 2 +- .../Sources/GiftOptionsScreen.swift | 7 +- .../Sources/GiftSetupScreen.swift | 12 +- .../Sources/GiftViewScreen.swift | 10 +- .../PeerInfoScreenPersonalChannelItem.swift | 3 + .../Sources/PeerInfoGiftsPaneNode.swift | 2 +- .../Settings/AccountFreezeInfoScreen/BUILD | 36 ++ .../Sources/AccountFreezeInfoScreen.swift | 578 ++++++++++++++++++ ...aticBusinessMessageListItemComponent.swift | 2 + .../Sources/QuickReplySetupScreen.swift | 2 + .../ThemeAccentColorControllerNode.swift | 1 + .../Sources/StarsTransferScreen.swift | 11 +- .../Appeal.imageset/Contents.json | 12 + .../Appeal.imageset/hand_30.pdf | Bin 0 -> 4643 bytes .../Account Freeze/Contents.json | 9 + .../Violation.imageset/Contents.json | 12 + .../Violation.imageset/sandtimer_30.pdf | Bin 0 -> 4669 bytes .../Components/AdMock.imageset/Contents.json | 21 + .../Components/AdMock.imageset/admock.png | Bin 0 -> 4477 bytes .../Components/PayMock.imageset/Contents.json | 12 + .../Components/PayMock.imageset/MockSMS.png | Bin 0 -> 88276 bytes .../TelegramUI/Sources/AccountContext.swift | 5 +- .../Chat/ChatControllerLoadDisplayNode.swift | 13 +- .../Chat/ChatControllerPaidMessage.swift | 5 +- .../ChatInterfaceStateInputPanels.swift | 12 + .../ChatReportPeerTitlePanelNode.swift | 6 +- .../ChatRestrictedInputPanelNode.swift | 35 +- .../ChatSearchResultsContollerNode.swift | 1 + .../CommandChatInputContextPanelNode.swift | 2 + .../Sources/SharedAccountContext.swift | 5 + 75 files changed, 1653 insertions(+), 126 deletions(-) create mode 100644 submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift create mode 100644 submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/BUILD create mode 100644 submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/Sources/AccountFreezeInfoScreen.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/hand_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Account Freeze/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/sandtimer_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Components/AdMock.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Components/AdMock.imageset/admock.png create mode 100644 submodules/TelegramUI/Images.xcassets/Components/PayMock.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Components/PayMock.imageset/MockSMS.png diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 9cd23dc708..eacb803483 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1129,6 +1129,8 @@ public protocol SharedAccountContext: AnyObject { func makeGalleryController(context: AccountContext, source: GalleryControllerItemSource, streamSingleVideo: Bool, isPreview: Bool) -> ViewController + func makeAccountFreezeInfoScreen(context: AccountContext) -> ViewController + func makeDebugSettingsController(context: AccountContext?) -> ViewController? func navigateToCurrentCall() diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 15277eccbc..86a92800d1 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -294,6 +294,44 @@ public struct PremiumConfiguration { } } +public struct AccountFreezeConfiguration { + public static var defaultValue: AccountFreezeConfiguration { + return AccountFreezeConfiguration( + freezeSinceDate: nil, + freezeUntilDate: nil, + freezeAppealUrl: nil + ) + } + + public let freezeSinceDate: Int32? + public let freezeUntilDate: Int32? + public let freezeAppealUrl: String? + + fileprivate init( + freezeSinceDate: Int32?, + freezeUntilDate: Int32?, + freezeAppealUrl: String? + ) { + self.freezeSinceDate = freezeSinceDate + self.freezeUntilDate = freezeUntilDate + self.freezeAppealUrl = freezeAppealUrl + } + + public static func with(appConfiguration: AppConfiguration) -> AccountFreezeConfiguration { + let defaultValue = self.defaultValue + if let data = appConfiguration.data { + return AccountFreezeConfiguration( + freezeSinceDate: (data["freeze_since_date"] as? Double).flatMap(Int32.init) ?? defaultValue.freezeSinceDate, + freezeUntilDate: (data["freeze_until_date"] as? Double).flatMap(Int32.init) ?? defaultValue.freezeUntilDate, + freezeAppealUrl: data["freeze_appeal_url"] as? String ?? defaultValue.freezeAppealUrl + ) + } else { + return defaultValue + } + } +} + + public protocol GiftOptionsScreenProtocol { } diff --git a/submodules/AuthorizationUI/BUILD b/submodules/AuthorizationUI/BUILD index f941572f4b..db3854ecc2 100644 --- a/submodules/AuthorizationUI/BUILD +++ b/submodules/AuthorizationUI/BUILD @@ -42,6 +42,7 @@ swift_library( "//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities", "//submodules/MoreButtonNode:MoreButtonNode", "//submodules/ContextUI:ContextUI", + "//submodules/InAppPurchaseManager", ], visibility = [ "//visibility:public", diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index 293e7475b9..47c1c5b875 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -20,6 +20,7 @@ import TelegramNotices import AuthenticationServices import Markdown import AlertUI +import InAppPurchaseManager import ObjectiveC private var ObjCKey_Delegate: Int? @@ -59,6 +60,8 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth return TelegramEngineUnauthorized(account: self.account) } + private var inAppPurchaseManager: InAppPurchaseManager! + public init(sharedContext: SharedAccountContext, account: UnauthorizedAccount, otherAccountPhoneNumbers: ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]), presentationData: PresentationData, openUrl: @escaping (String) -> Void, apiId: Int32, apiHash: String, authorizationCompleted: @escaping () -> Void) { self.sharedContext = sharedContext self.account = account @@ -79,6 +82,8 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth super.init(mode: .single, theme: NavigationControllerTheme(statusBar: navigationStatusBar, navigationBar: AuthorizationSequenceController.navigationBarTheme(presentationData.theme), emptyAreaColor: .black), isFlat: true) + self.inAppPurchaseManager = InAppPurchaseManager(engine: .unauthorized(self.engine)) + self.stateDisposable = (self.engine.auth.state() |> map { state -> InnerState in if case .authorized = state { @@ -758,6 +763,18 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth return controller } + private func paymentController(number: String, phoneCodeHash: String, storeProduct: String) -> AuthorizationSequencePaymentScreen { + let controller = AuthorizationSequencePaymentScreen(engine: self.engine, presentationData: self.presentationData, inAppPurchaseManager: self.inAppPurchaseManager, phoneNumber: number, phoneCodeHash: phoneCodeHash, storeProduct: storeProduct, back: { [weak self] in + guard let self else { + return + } + let countryCode = AuthorizationSequenceController.defaultCountryCode() + + let _ = self.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: self.account.testingEnvironment, masterDatacenterId: self.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() + }) + return controller + } + @available(iOS 13.0, *) public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { let lastController = self.viewControllers.last as? ViewController @@ -1285,6 +1302,13 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth } controllers.append(self.signUpController(firstName: firstName, lastName: lastName, termsOfService: termsOfService, displayCancel: displayCancel)) self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty) + case let .payment(number, codeHash, storeProduct): + var controllers: [ViewController] = [] + if !self.otherAccountPhoneNumbers.1.isEmpty { + controllers.append(self.splashController()) + } + controllers.append(self.paymentController(number: number, phoneCodeHash: codeHash, storeProduct: storeProduct)) + self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty) } } } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift new file mode 100644 index 0000000000..55efdae6f7 --- /dev/null +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift @@ -0,0 +1,287 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import TelegramStringFormatting +import PresentationDataUtils +import ComponentFlow +import ViewControllerComponent +import MultilineTextComponent +import BalancedTextComponent +import BundleIconComponent +import LottieComponent +import ButtonComponent +import TextFormat +import InAppPurchaseManager +import ConfettiEffect + +final class AuthorizationSequencePaymentScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let engine: TelegramEngineUnauthorized + let inAppPurchaseManager: InAppPurchaseManager + let presentationData: PresentationData + let phoneNumber: String + let phoneCodeHash: String + let storeProduct: String + + init( + engine: TelegramEngineUnauthorized, + inAppPurchaseManager: InAppPurchaseManager, + presentationData: PresentationData, + phoneNumber: String, + phoneCodeHash: String, + storeProduct: String + ) { + self.engine = engine + self.inAppPurchaseManager = inAppPurchaseManager + self.presentationData = presentationData + self.phoneNumber = phoneNumber + self.phoneCodeHash = phoneCodeHash + self.storeProduct = storeProduct + } + + static func ==(lhs: AuthorizationSequencePaymentScreenComponent, rhs: AuthorizationSequencePaymentScreenComponent) -> Bool { + if lhs.storeProduct != rhs.storeProduct { + return false + } + return true + } + + final class View: UIView { + private let animation = ComponentView() + private let title = ComponentView() + private let list = ComponentView() + private let check = ComponentView() + private let button = ComponentView() + + private var isUpdating: Bool = false + + private var component: AuthorizationSequencePaymentScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + private var products: [InAppPurchaseManager.Product] = [] + private var productsDisposable: Disposable? + private var inProgress = false + + override init(frame: CGRect) { + super.init(frame: frame) + + self.disablesInteractiveKeyboardGestureRecognizer = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.productsDisposable?.dispose() + } + + private func proceed() { + guard let component = self.component, let storeProduct = self.products.first(where: { $0.id == component.storeProduct }) else { + return + } + + self.inProgress = true + self.state?.updated() + + let (currency, amount) = storeProduct.priceCurrencyAndAmount + + let purpose: AppStoreTransactionPurpose = .authCode(restore: false, phoneNumber: component.phoneNumber, phoneCodeHash: component.phoneCodeHash, currency: currency, amount: amount) + + let _ = (component.engine.payments.canPurchasePremium(purpose: purpose) + |> deliverOnMainQueue).start(next: { [weak self] available in + guard let self else { + return + } + let presentationData = component.presentationData + if available { + let _ = (component.inAppPurchaseManager.buyProduct(storeProduct, quantity: 1, purpose: purpose) + |> deliverOnMainQueue).start(next: { [weak self] status in + let _ = status + let _ = self + }, error: { [weak self] error in + guard let self, let controller = self.environment?.controller() else { + return + } + self.state?.updated(transition: .immediate) + + var errorText: String? + switch error { + case .generic: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .network: + errorText = presentationData.strings.Premium_Purchase_ErrorNetwork + case .notAllowed: + errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed + case .cantMakePayments: + errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments + case .assignFailed: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .tryLater: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .cancelled: + break + } + + if let errorText { + //addAppLogEvent(postbox: component.engine.account.postbox, type: "premium_gift.promo_screen_fail") + + let _ = errorText + let _ = controller + //let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + //controller.present(alertController, in: .window(.root)) + } + }) + } else { + self.inProgress = false + self.state?.updated(transition: .immediate) + } + }) + } + + func update(component: AuthorizationSequencePaymentScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + self.component = component + self.state = state + + if self.component == nil { + self.productsDisposable = (component.inAppPurchaseManager.availableProducts + |> deliverOnMainQueue).start(next: { [weak self] products in + guard let self else { + return + } + self.products = products + self.state?.updated() + }) + } + + if themeUpdated { + self.backgroundColor = environment.theme.list.plainBackgroundColor + } + + let animationHeight: CGFloat = 120.0 + let animationSize = self.animation.update( + transition: transition, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "Coin"), + startingPosition: .begin + )), + environment: {}, + containerSize: CGSize(width: animationHeight, height: animationHeight) + ) + if let animationView = self.animation.view { + if animationView.superview == nil { + self.addSubview(animationView) + } + animationView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - animationSize.width) / 2.0), y: 156.0), size: animationSize) + } + + let buttonHeight: CGFloat = 50.0 + let bottomPanelPadding: CGFloat = 12.0 + let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding + let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset + + let sideInset: CGFloat = 16.0 + let buttonString = "Sign up for $1" + let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + let buttonSize = self.button.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 10.0 + ), + content: AnyComponentWithIdentity( + id: AnyHashable(buttonString), + component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) + ), + isEnabled: true, + displaysProgress: self.inProgress, + action: { [weak self] in + self?.proceed() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: buttonHeight) + ) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - bottomPanelHeight + bottomPanelPadding), size: buttonSize) + } + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class AuthorizationSequencePaymentScreen: ViewControllerComponentContainer { + public init( + engine: TelegramEngineUnauthorized, + presentationData: PresentationData, + inAppPurchaseManager: InAppPurchaseManager, + phoneNumber: String, + phoneCodeHash: String, + storeProduct: String, + back: @escaping () -> Void + ) { + super.init(component: AuthorizationSequencePaymentScreenComponent( + engine: engine, + inAppPurchaseManager: inAppPurchaseManager, + presentationData: presentationData, + phoneNumber: phoneNumber, + phoneCodeHash: phoneCodeHash, + storeProduct: storeProduct + ), navigationBarAppearance: .transparent, theme: .default, updatedPresentationData: (initial: presentationData, signal: .single(presentationData))) + + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + + self.attemptNavigation = { _ in + return false + } + self.navigationBar?.backPressed = { + back() + } + } + + public override func loadDisplayNode() { + super.loadDisplayNode() + + self.displayNode.view.disableAutomaticKeyboardHandling = [.forward, .backward] + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func cancelPressed() { + self.dismiss() + } +} diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 9a07996286..b567a0a2a8 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1208,6 +1208,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController ) } + self.chatListDisplayNode.mainContainerNode.openAccountFreezeInfo = { [weak self] in + guard let self else { + return + } + let controller = self.context.sharedContext.makeAccountFreezeInfoScreen(context: self.context) + self.push(controller) + } + self.chatListDisplayNode.mainContainerNode.openPhotoSetup = { [weak self] in guard let self else { return diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 6f71a800bd..ef7d0214de 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -357,6 +357,9 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele itemNode.listNode.openPhotoSetup = { [weak self] in self?.openPhotoSetup?() } + itemNode.listNode.openAccountFreezeInfo = { [weak self] in + self?.openAccountFreezeInfo?() + } self.currentItemStateValue.set(itemNode.listNode.state |> map { state in let filterId: Int32? @@ -425,6 +428,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele var openStarsTopup: ((Int64?) -> Void)? var openWebApp: ((TelegramUser) -> Void)? var openPhotoSetup: (() -> Void)? + var openAccountFreezeInfo: (() -> Void)? var addedVisibleChatsWithPeerIds: (([EnginePeer.Id]) -> Void)? var didBeginSelectingChats: (() -> Void)? var canExpandHiddenItems: (() -> Bool)? diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index b3c32c75b6..19632523ba 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -3261,6 +3261,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, openPhotoSetup: { }, openAdInfo: { node in interaction.openAdInfo(node) + }, openAccountFreezeInfo: { }) chatListInteraction.isSearchMode = true @@ -5243,6 +5244,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { }, openWebApp: { _ in }, openPhotoSetup: { }, openAdInfo: { _ in + }, openAccountFreezeInfo: { }) var isInlineMode = false if case .topics = key { diff --git a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift index 1f5727d75f..cc9dcbf111 100644 --- a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift @@ -162,6 +162,7 @@ public final class ChatListShimmerNode: ASDisplayNode { }, openWebApp: { _ in }, openPhotoSetup: { }, openAdInfo: { _ in + }, openAccountFreezeInfo: { }) interaction.isInlineMode = isInlineMode diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 0e83bccb9e..8175610721 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -115,6 +115,7 @@ public final class ChatListNodeInteraction { let openWebApp: (TelegramUser) -> Void let openPhotoSetup: () -> Void let openAdInfo: (ASDisplayNode) -> Void + let openAccountFreezeInfo: () -> Void public var searchTextHighightState: String? var highlightedChatLocation: ChatListHighlightedLocation? @@ -173,7 +174,8 @@ public final class ChatListNodeInteraction { editPeer: @escaping (ChatListItem) -> Void, openWebApp: @escaping (TelegramUser) -> Void, openPhotoSetup: @escaping () -> Void, - openAdInfo: @escaping (ASDisplayNode) -> Void + openAdInfo: @escaping (ASDisplayNode) -> Void, + openAccountFreezeInfo: @escaping () -> Void ) { self.activateSearch = activateSearch self.peerSelected = peerSelected @@ -220,6 +222,7 @@ public final class ChatListNodeInteraction { self.openWebApp = openWebApp self.openPhotoSetup = openPhotoSetup self.openAdInfo = openAdInfo + self.openAccountFreezeInfo = openAccountFreezeInfo } } @@ -770,6 +773,8 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL nodeInteraction?.openStarsTopup(amount.value) case .setupPhoto: nodeInteraction?.openPhotoSetup() + case .accountFreeze: + nodeInteraction?.openAccountFreezeInfo() } case .hide: nodeInteraction?.dismissNotice(notice) @@ -1116,6 +1121,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL nodeInteraction?.openStarsTopup(amount.value) case .setupPhoto: nodeInteraction?.openPhotoSetup() + case .accountFreeze: + nodeInteraction?.openAccountFreezeInfo() } case .hide: nodeInteraction?.dismissNotice(notice) @@ -1239,6 +1246,7 @@ public final class ChatListNode: ListView { public var openWebApp: ((TelegramUser) -> Void)? public var openPhotoSetup: (() -> Void)? public var openAdInfo: ((ASDisplayNode) -> Void)? + public var openAccountFreezeInfo: (() -> Void)? private var theme: PresentationTheme @@ -1899,6 +1907,8 @@ public final class ChatListNode: ListView { self.openPhotoSetup?() }, openAdInfo: { [weak self] node in self?.openAdInfo?(node) + }, openAccountFreezeInfo: { [weak self] in + self?.openAccountFreezeInfo?() }) nodeInteraction.isInlineMode = isInlineMode @@ -1988,6 +1998,16 @@ public final class ChatListNode: ListView { let twoStepData: Signal = .single(nil) |> then(context.engine.auth.twoStepVerificationConfiguration() |> map(Optional.init)) + let accountFreezeConfiguration = (context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) + |> map { view -> AppConfiguration in + let appConfiguration: AppConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue + return appConfiguration + } + |> distinctUntilChanged + |> map { appConfiguration -> AccountFreezeConfiguration in + return AccountFreezeConfiguration.with(appConfiguration: appConfiguration) + }) + let suggestedChatListNoticeSignal: Signal = combineLatest( context.engine.notices.getServerProvidedSuggestions(), context.engine.notices.getServerDismissedSuggestions(), @@ -1998,11 +2018,12 @@ public final class ChatListNode: ListView { TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId) ), context.account.stateManager.contactBirthdays, - starsSubscriptionsContextPromise.get() + starsSubscriptionsContextPromise.get(), + accountFreezeConfiguration ) - |> mapToSignal { suggestions, dismissedSuggestions, configuration, newSessionReviews, data, birthdays, starsSubscriptionsContext -> Signal in + |> mapToSignal { suggestions, dismissedSuggestions, configuration, newSessionReviews, data, birthdays, starsSubscriptionsContext, accountFreezeConfiguration -> Signal in let (accountPeer, birthday) = data - + if let newSessionReview = newSessionReviews.first { return .single(.reviewLogin(newSessionReview: newSessionReview, totalCount: newSessionReviews.count)) } @@ -2035,8 +2056,10 @@ public final class ChatListNode: ListView { if dismissedSuggestions.contains(.todayBirthdays) { todayBirthdayPeerIds = [] } - - if suggestions.contains(.starsSubscriptionLowBalance) { + + if let _ = accountFreezeConfiguration.freezeUntilDate { + return .single(.accountFreeze) + } else if suggestions.contains(.starsSubscriptionLowBalance) { if let starsSubscriptionsContext { return starsSubscriptionsContext.state |> map { state in diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index 4a338533a2..9a839cdb9c 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -92,6 +92,7 @@ public enum ChatListNotice: Equatable { case premiumGrace case starsSubscriptionLowBalance(amount: StarsAmount, peers: [EnginePeer]) case setupPhoto(EnginePeer) + case accountFreeze } enum ChatListNodeEntry: Comparable, Identifiable { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift index e1a1f44eba..14f4e82614 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift @@ -288,6 +288,10 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { titleString = NSAttributedString(string: item.strings.ChatList_AddPhoto_Title, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor) textString = NSAttributedString(string: item.strings.ChatList_AddPhoto_Text, font: smallTextFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) avatarPeer = accountPeer + case .accountFreeze: + //TODO:localize + titleString = NSAttributedString(string: "Your account is frozen", font: titleFont, textColor: item.theme.list.itemDestructiveColor) + textString = NSAttributedString(string: "Tap to view details and submit an appeal.", font: smallTextFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) } var leftInset: CGFloat = sideInset diff --git a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift index b8955192e2..e3b6141e82 100644 --- a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift +++ b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift @@ -151,8 +151,8 @@ open class ViewControllerComponentContainer: ViewController { private var currentIsVisible: Bool = false private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? - init(context: AccountContext, controller: ViewControllerComponentContainer, component: AnyComponent, theme: Theme) { - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + init(presentationData: PresentationData, controller: ViewControllerComponentContainer, component: AnyComponent, theme: Theme) { + self.presentationData = presentationData self.controller = controller @@ -234,7 +234,7 @@ open class ViewControllerComponentContainer: ViewController { return self.displayNode as! Node } - private let context: AccountContext + private var presentationData: PresentationData private var theme: Theme public private(set) var component: AnyComponent @@ -252,17 +252,19 @@ open class ViewControllerComponentContainer: ViewController { theme: Theme = .default, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil ) where C.EnvironmentType == ViewControllerComponentContainer.Environment { - self.context = context self.component = AnyComponent(component) self.theme = theme - let presentationData: PresentationData + var effectiveUpdatedPresentationData: (initial: PresentationData, signal: Signal) if let updatedPresentationData { - presentationData = updatedPresentationData.initial + effectiveUpdatedPresentationData = updatedPresentationData } else { - presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + effectiveUpdatedPresentationData = (initial: context.sharedContext.currentPresentationData.with { $0 }, signal: context.sharedContext.presentationData) } + let presentationData = effectiveUpdatedPresentationData.initial + self.presentationData = presentationData + let navigationBarPresentationData: NavigationBarPresentationData? switch navigationBarAppearance { case .none: @@ -274,7 +276,47 @@ open class ViewControllerComponentContainer: ViewController { } super.init(navigationBarPresentationData: navigationBarPresentationData) - self.presentationDataDisposable = ((updatedPresentationData?.signal ?? self.context.sharedContext.presentationData) + self.setupPresentationData(effectiveUpdatedPresentationData, navigationBarAppearance: navigationBarAppearance, statusBarStyle: statusBarStyle, presentationMode: presentationMode) + } + + public init( + component: C, + navigationBarAppearance: NavigationBarAppearance, + statusBarStyle: StatusBarStyle = .default, + presentationMode: PresentationMode = .default, + theme: Theme = .default, + updatedPresentationData: (initial: PresentationData, signal: Signal) + ) where C.EnvironmentType == ViewControllerComponentContainer.Environment { + self.component = AnyComponent(component) + self.theme = theme + + let presentationData = updatedPresentationData.initial + self.presentationData = presentationData + + let navigationBarPresentationData: NavigationBarPresentationData? + switch navigationBarAppearance { + case .none: + navigationBarPresentationData = nil + case .transparent: + navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData, hideBackground: true, hideBadge: false, hideSeparator: true) + case .default: + navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData) + } + super.init(navigationBarPresentationData: navigationBarPresentationData) + + self.setupPresentationData(updatedPresentationData, navigationBarAppearance: navigationBarAppearance, statusBarStyle: statusBarStyle, presentationMode: presentationMode) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + private func setupPresentationData(_ updatedPresentationData: (initial: PresentationData, signal: Signal), navigationBarAppearance: NavigationBarAppearance, statusBarStyle: StatusBarStyle, presentationMode: PresentationMode) { + self.presentationDataDisposable = (updatedPresentationData.signal |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { var theme = presentationData.theme @@ -329,16 +371,8 @@ open class ViewControllerComponentContainer: ViewController { } } - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.presentationDataDisposable?.dispose() - } - override open func loadDisplayNode() { - self.displayNode = Node(context: self.context, controller: self, component: self.component, theme: self.theme) + self.displayNode = Node(presentationData: self.presentationData, controller: self, component: self.component, theme: self.theme) self.displayNodeDidLoad() } diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index f33c2c5dba..4e29b6eb61 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -591,7 +591,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis allSelected = false } var actionTitle: String? - if peerIds.count > 1 { + if !"".isEmpty, peerIds.count > 1 { actionTitle = allSelected ? strings.Premium_Gift_ContactSelection_DeselectAll.uppercased() : strings.Premium_Gift_ContactSelection_SelectAll.uppercased() } let header: ListViewItemHeader? = ChatListSearchItemHeader(type: .text(title.uppercased(), AnyHashable(10 * sectionId + (allSelected ? 1 : 0))), theme: theme, strings: strings, actionTitle: actionTitle, action: { _ in diff --git a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift index 0c955d674c..2bb095f7f4 100644 --- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift +++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift @@ -216,7 +216,7 @@ public final class InAppPurchaseManager: NSObject { case deferred } - private let engine: TelegramEngine + private let engine: SomeTelegramEngine private var products: [Product] = [] private var productsPromise = Promise<[Product]>([]) @@ -231,7 +231,9 @@ public final class InAppPurchaseManager: NSObject { private let disposableSet = DisposableDict() - public init(engine: TelegramEngine) { + private var lastRequestTimestamp: Double? + + public init(engine: SomeTelegramEngine) { self.engine = engine super.init() @@ -255,11 +257,15 @@ public final class InAppPurchaseManager: NSObject { productRequest.start() self.productRequest = productRequest + self.lastRequestTimestamp = CFAbsoluteTimeGetCurrent() } public var availableProducts: Signal<[Product], NoError> { - if self.products.isEmpty && self.productRequest == nil { - self.requestProducts() + if self.products.isEmpty { + if let lastRequestTimestamp, CFAbsoluteTimeGetCurrent() - lastRequestTimestamp > 10.0 { + Logger.shared.log("InAppPurchaseManager", "No available products, rerequest") + self.requestProducts() + } } return self.productsPromise.get() } @@ -287,7 +293,13 @@ public final class InAppPurchaseManager: NSObject { return .fail(.cantMakePayments) } - let accountPeerId = "\(self.engine.account.peerId.toInt64())" + let accountPeerId: String + switch self.engine { + case let .authorized(engine): + accountPeerId = "\(engine.account.peerId.toInt64())" + case let .unauthorized(engine): + accountPeerId = "\(engine.account.id.int64)" + } Logger.shared.log("InAppPurchaseManager", "Buying: account \(accountPeerId), product \(product.skProduct.productIdentifier), price \(product.price)") @@ -399,7 +411,13 @@ private func getReceiptData() -> Data? { extension InAppPurchaseManager: SKPaymentTransactionObserver { public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { self.stateQueue.async { - let accountPeerId = "\(self.engine.account.peerId.toInt64())" + let accountPeerId: String + switch self.engine { + case let .authorized(engine): + accountPeerId = "\(engine.account.peerId.toInt64())" + case let .unauthorized(engine): + accountPeerId = "\(engine.account.id.int64)" + } let paymentContexts = self.paymentContexts @@ -519,7 +537,12 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver { (purpose |> castError(AssignAppStoreTransactionError.self) |> mapToSignal { purpose -> Signal in - return self.engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: purpose) + switch self.engine { + case let .authorized(engine): + return engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: purpose) + case let .unauthorized(engine): + return engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: purpose) + } }).start(error: { [weak self] _ in Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transactions [\(transactionIds)] failed to assign") for transaction in transactions { @@ -551,8 +574,15 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver { self.onRestoreCompletion = nil if let receiptData = getReceiptData() { + let signal: Signal + switch self.engine { + case let .authorized(engine): + signal = engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: .restore) + case let .unauthorized(engine): + signal = engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: .restore) + } self.disposableSet.set( - self.engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: .restore).start(error: { error in + signal.start(error: { error in Queue.mainQueue().async { if case .serverProvided = error { onRestoreCompletion(.succeed(true)) @@ -586,14 +616,17 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver { } private func debugSaveReceipt(receiptData: Data) { + guard case let .authorized(engine) = self.engine else { + return + } let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: Int64(receiptData.count), isSecretRelated: false) - self.engine.account.postbox.mediaBox.storeResourceData(fileResource.id, data: receiptData) + engine.account.postbox.mediaBox.storeResourceData(fileResource.id, data: receiptData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(receiptData.count), attributes: [.FileName(fileName: "Receipt.dat")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - let _ = enqueueMessages(account: self.engine.account, peerId: self.engine.account.peerId, messages: [message]).start() + let _ = enqueueMessages(account: engine.account, peerId: engine.account.peerId, messages: [message]).start() } } @@ -625,6 +658,9 @@ private final class PendingInAppPurchaseState: Codable { case users case text case entities + case restore + case phoneNumber + case phoneCodeHash } enum PurposeType: Int32 { @@ -637,6 +673,7 @@ private final class PendingInAppPurchaseState: Codable { case stars case starsGift case starsGiveaway + case authCode } case subscription @@ -648,6 +685,7 @@ private final class PendingInAppPurchaseState: Codable { case stars(count: Int64) case starsGift(peerId: EnginePeer.Id, count: Int64) case starsGiveaway(stars: Int64, boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, users: Int32) + case authCode(restore: Bool, phoneNumber: String, phoneCodeHash: String) public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -704,6 +742,12 @@ private final class PendingInAppPurchaseState: Codable { untilDate: try container.decode(Int32.self, forKey: .untilDate), users: try container.decode(Int32.self, forKey: .users) ) + case .authCode: + self = .authCode( + restore: try container.decode(Bool.self, forKey: .restore), + phoneNumber: try container.decode(String.self, forKey: .phoneNumber), + phoneCodeHash: try container.decode(String.self, forKey: .phoneCodeHash) + ) default: throw DecodingError.generic } @@ -757,6 +801,11 @@ private final class PendingInAppPurchaseState: Codable { try container.encode(randomId, forKey: .randomId) try container.encode(untilDate, forKey: .untilDate) try container.encode(users, forKey: .users) + case let .authCode(restore, phoneNumber, phoneCodeHash): + try container.encode(PurposeType.authCode.rawValue, forKey: .type) + try container.encode(restore, forKey: .restore) + try container.encode(phoneNumber, forKey: .phoneNumber) + try container.encode(phoneCodeHash, forKey: .phoneCodeHash) } } @@ -780,6 +829,8 @@ private final class PendingInAppPurchaseState: Codable { self = .starsGift(peerId: peerId, count: count) case let .starsGiveaway(stars, boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, _, _, users): self = .starsGiveaway(stars: stars, boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, users: users) + case let .authCode(restore, phoneNumber, phoneCodeHash, _, _): + self = .authCode(restore: restore, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash) } } @@ -804,6 +855,8 @@ private final class PendingInAppPurchaseState: Codable { return .starsGift(peerId: peerId, count: count, currency: currency, amount: amount) case let .starsGiveaway(stars, boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, users): return .starsGiveaway(stars: stars, boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount, users: users) + case let .authCode(restore, phoneNumber, phoneCodeHash): + return .authCode(restore: restore, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, currency: currency, amount: amount) } } } @@ -831,23 +884,41 @@ private final class PendingInAppPurchaseState: Codable { } } -private func pendingInAppPurchaseState(engine: TelegramEngine, productId: String) -> Signal { +private func pendingInAppPurchaseState(engine: SomeTelegramEngine, productId: String) -> Signal { let key = EngineDataBuffer(length: 8) key.setInt64(0, value: Int64(bitPattern: productId.persistentHashValue)) - return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key)) - |> map { entry -> PendingInAppPurchaseState? in - return entry?.get(PendingInAppPurchaseState.self) + switch engine { + case let .authorized(engine): + return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key)) + |> map { entry -> PendingInAppPurchaseState? in + return entry?.get(PendingInAppPurchaseState.self) + } + case let .unauthorized(engine): + return engine.itemCache.get(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key) + |> map { entry -> PendingInAppPurchaseState? in + return entry?.get(PendingInAppPurchaseState.self) + } } } -private func updatePendingInAppPurchaseState(engine: TelegramEngine, productId: String, content: PendingInAppPurchaseState?) -> Signal { +private func updatePendingInAppPurchaseState(engine: SomeTelegramEngine, productId: String, content: PendingInAppPurchaseState?) -> Signal { let key = EngineDataBuffer(length: 8) key.setInt64(0, value: Int64(bitPattern: productId.persistentHashValue)) - if let content = content { - return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key, item: content) - } else { - return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key) + + switch engine { + case let .authorized(engine): + if let content = content { + return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key, item: content) + } else { + return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key) + } + case let .unauthorized(engine): + if let content = content { + return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key, item: content) + } else { + return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key) + } } } diff --git a/submodules/MtProtoKit/Sources/MTRequestMessageService.m b/submodules/MtProtoKit/Sources/MTRequestMessageService.m index 27100eeecd..99369c796c 100644 --- a/submodules/MtProtoKit/Sources/MTRequestMessageService.m +++ b/submodules/MtProtoKit/Sources/MTRequestMessageService.m @@ -898,7 +898,7 @@ } restartRequest = true; } - else if (rpcError.errorCode == 420 || [rpcError.errorDescription rangeOfString:@"FLOOD_WAIT_"].location != NSNotFound || [rpcError.errorDescription rangeOfString:@"FLOOD_PREMIUM_WAIT_"].location != NSNotFound) { + else if ((rpcError.errorCode == 420 && [rpcError.errorDescription rangeOfString:@"FROZEN_METHOD_INVALID"].location == NSNotFound) || [rpcError.errorDescription rangeOfString:@"FLOOD_WAIT_"].location != NSNotFound || [rpcError.errorDescription rangeOfString:@"FLOOD_PREMIUM_WAIT_"].location != NSNotFound) { if (request.errorContext == nil) request.errorContext = [[MTRequestErrorContext alloc] init]; diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index cdddb1136c..09079638a9 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -233,6 +233,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView }, openWebApp: { _ in }, openPhotoSetup: { }, openAdInfo: { _ in + }, openAccountFreezeInfo: { }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index b93df4e10c..f2ae983894 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -382,6 +382,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { }, openWebApp: { _ in }, openPhotoSetup: { }, openAdInfo: { _ in + }, openAccountFreezeInfo: { }) func makeChatListItem( diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 06fbfc611d..ddc57bf86d 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -84,7 +84,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1778593322] = { return Api.BotApp.parse_botApp($0) } dict[1571189943] = { return Api.BotApp.parse_botAppNotModified($0) } dict[-912582320] = { return Api.BotAppSettings.parse_botAppSettings($0) } - dict[-1989921868] = { return Api.BotBusinessConnection.parse_botBusinessConnection($0) } + dict[-1892371723] = { return Api.BotBusinessConnection.parse_botBusinessConnection($0) } dict[-1032140601] = { return Api.BotCommand.parse_botCommand($0) } dict[-1180016534] = { return Api.BotCommandScope.parse_botCommandScopeChatAdmins($0) } dict[1877059713] = { return Api.BotCommandScope.parse_botCommandScopeChats($0) } @@ -118,6 +118,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-867328308] = { return Api.BusinessAwayMessageSchedule.parse_businessAwayMessageScheduleCustom($0) } dict[-1007487743] = { return Api.BusinessAwayMessageSchedule.parse_businessAwayMessageScheduleOutsideWorkHours($0) } dict[-1198722189] = { return Api.BusinessBotRecipients.parse_businessBotRecipients($0) } + dict[-1604170505] = { return Api.BusinessBotRights.parse_businessBotRights($0) } dict[-1263638929] = { return Api.BusinessChatLink.parse_businessChatLink($0) } dict[-451302485] = { return Api.BusinessGreetingMessage.parse_businessGreetingMessage($0) } dict[1510606445] = { return Api.BusinessIntro.parse_businessIntro($0) } @@ -224,7 +225,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1713193015] = { return Api.ChatReactions.parse_chatReactionsSome($0) } dict[-1390068360] = { return Api.CodeSettings.parse_codeSettings($0) } dict[-870702050] = { return Api.Config.parse_config($0) } - dict[-1123645951] = { return Api.ConnectedBot.parse_connectedBot($0) } + dict[-849058964] = { return Api.ConnectedBot.parse_connectedBot($0) } dict[429997937] = { return Api.ConnectedBotStarRef.parse_connectedBotStarRef($0) } dict[341499403] = { return Api.Contact.parse_contact($0) } dict[496600883] = { return Api.ContactBirthday.parse_contactBirthday($0) } @@ -480,6 +481,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[853188252] = { return Api.InputStickerSetItem.parse_inputStickerSetItem($0) } dict[70813275] = { return Api.InputStickeredMedia.parse_inputStickeredMediaDocument($0) } dict[1251549527] = { return Api.InputStickeredMedia.parse_inputStickeredMediaPhoto($0) } + dict[-1682807955] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentAuthCode($0) } dict[1634697192] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentGiftPremium($0) } dict[-75955309] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiftCode($0) } dict[369444042] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiveaway($0) } @@ -1110,6 +1112,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1821035490] = { return Api.Update.parse_updateSavedGifs($0) } dict[969307186] = { return Api.Update.parse_updateSavedReactionTags($0) } dict[1960361625] = { return Api.Update.parse_updateSavedRingtones($0) } + dict[1347068303] = { return Api.Update.parse_updateSentPhoneCode($0) } dict[2103604867] = { return Api.Update.parse_updateSentStoryReaction($0) } dict[-337352679] = { return Api.Update.parse_updateServiceNotification($0) } dict[-245208620] = { return Api.Update.parse_updateSmsJob($0) } @@ -1217,6 +1220,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[957176926] = { return Api.auth.LoginToken.parse_loginTokenSuccess($0) } dict[326715557] = { return Api.auth.PasswordRecovery.parse_passwordRecovery($0) } dict[1577067778] = { return Api.auth.SentCode.parse_sentCode($0) } + dict[304435204] = { return Api.auth.SentCode.parse_sentCodePaymentRequired($0) } dict[596704836] = { return Api.auth.SentCode.parse_sentCodeSuccess($0) } dict[1035688326] = { return Api.auth.SentCodeType.parse_sentCodeTypeApp($0) } dict[1398007207] = { return Api.auth.SentCodeType.parse_sentCodeTypeCall($0) } @@ -1589,6 +1593,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.BusinessBotRecipients: _1.serialize(buffer, boxed) + case let _1 as Api.BusinessBotRights: + _1.serialize(buffer, boxed) case let _1 as Api.BusinessChatLink: _1.serialize(buffer, boxed) case let _1 as Api.BusinessGreetingMessage: diff --git a/submodules/TelegramApi/Sources/Api1.swift b/submodules/TelegramApi/Sources/Api1.swift index 04b97a79c0..f6e3fd0d8a 100644 --- a/submodules/TelegramApi/Sources/Api1.swift +++ b/submodules/TelegramApi/Sources/Api1.swift @@ -1170,27 +1170,28 @@ public extension Api { } public extension Api { enum BotBusinessConnection: TypeConstructorDescription { - case botBusinessConnection(flags: Int32, connectionId: String, userId: Int64, dcId: Int32, date: Int32) + case botBusinessConnection(flags: Int32, connectionId: String, userId: Int64, dcId: Int32, date: Int32, rights: Api.BusinessBotRights?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .botBusinessConnection(let flags, let connectionId, let userId, let dcId, let date): + case .botBusinessConnection(let flags, let connectionId, let userId, let dcId, let date, let rights): if boxed { - buffer.appendInt32(-1989921868) + buffer.appendInt32(-1892371723) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(connectionId, buffer: buffer, boxed: false) serializeInt64(userId, buffer: buffer, boxed: false) serializeInt32(dcId, buffer: buffer, boxed: false) serializeInt32(date, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {rights!.serialize(buffer, true)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .botBusinessConnection(let flags, let connectionId, let userId, let dcId, let date): - return ("botBusinessConnection", [("flags", flags as Any), ("connectionId", connectionId as Any), ("userId", userId as Any), ("dcId", dcId as Any), ("date", date as Any)]) + case .botBusinessConnection(let flags, let connectionId, let userId, let dcId, let date, let rights): + return ("botBusinessConnection", [("flags", flags as Any), ("connectionId", connectionId as Any), ("userId", userId as Any), ("dcId", dcId as Any), ("date", date as Any), ("rights", rights as Any)]) } } @@ -1205,13 +1206,18 @@ public extension Api { _4 = reader.readInt32() var _5: Int32? _5 = reader.readInt32() + var _6: Api.BusinessBotRights? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.BusinessBotRights + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.BotBusinessConnection.botBusinessConnection(flags: _1!, connectionId: _2!, userId: _3!, dcId: _4!, date: _5!) + let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.BotBusinessConnection.botBusinessConnection(flags: _1!, connectionId: _2!, userId: _3!, dcId: _4!, date: _5!, rights: _6) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api13.swift b/submodules/TelegramApi/Sources/Api13.swift index 08c3149890..d0c6d7ee81 100644 --- a/submodules/TelegramApi/Sources/Api13.swift +++ b/submodules/TelegramApi/Sources/Api13.swift @@ -116,6 +116,7 @@ public extension Api { } public extension Api { indirect enum InputStorePaymentPurpose: TypeConstructorDescription { + case inputStorePaymentAuthCode(flags: Int32, phoneNumber: String, phoneCodeHash: String, currency: String, amount: Int64) case inputStorePaymentGiftPremium(userId: Api.InputUser, currency: String, amount: Int64) case inputStorePaymentPremiumGiftCode(flags: Int32, users: [Api.InputUser], boostPeer: Api.InputPeer?, currency: String, amount: Int64, message: Api.TextWithEntities?) case inputStorePaymentPremiumGiveaway(flags: Int32, boostPeer: Api.InputPeer, additionalPeers: [Api.InputPeer]?, countriesIso2: [String]?, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64) @@ -126,6 +127,16 @@ public extension Api { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { + case .inputStorePaymentAuthCode(let flags, let phoneNumber, let phoneCodeHash, let currency, let amount): + if boxed { + buffer.appendInt32(-1682807955) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(phoneNumber, buffer: buffer, boxed: false) + serializeString(phoneCodeHash, buffer: buffer, boxed: false) + serializeString(currency, buffer: buffer, boxed: false) + serializeInt64(amount, buffer: buffer, boxed: false) + break case .inputStorePaymentGiftPremium(let userId, let currency, let amount): if boxed { buffer.appendInt32(1634697192) @@ -223,6 +234,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { + case .inputStorePaymentAuthCode(let flags, let phoneNumber, let phoneCodeHash, let currency, let amount): + return ("inputStorePaymentAuthCode", [("flags", flags as Any), ("phoneNumber", phoneNumber as Any), ("phoneCodeHash", phoneCodeHash as Any), ("currency", currency as Any), ("amount", amount as Any)]) case .inputStorePaymentGiftPremium(let userId, let currency, let amount): return ("inputStorePaymentGiftPremium", [("userId", userId as Any), ("currency", currency as Any), ("amount", amount as Any)]) case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount, let message): @@ -240,6 +253,29 @@ public extension Api { } } + public static func parse_inputStorePaymentAuthCode(_ reader: BufferReader) -> InputStorePaymentPurpose? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: String? + _4 = parseString(reader) + var _5: Int64? + _5 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.InputStorePaymentPurpose.inputStorePaymentAuthCode(flags: _1!, phoneNumber: _2!, phoneCodeHash: _3!, currency: _4!, amount: _5!) + } + else { + return nil + } + } public static func parse_inputStorePaymentGiftPremium(_ reader: BufferReader) -> InputStorePaymentPurpose? { var _1: Api.InputUser? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index 4d6ba7fbaf..3947b37442 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -1103,6 +1103,7 @@ public extension Api { case updateSavedGifs case updateSavedReactionTags case updateSavedRingtones + case updateSentPhoneCode(sentCode: Api.auth.SentCode) case updateSentStoryReaction(peer: Api.Peer, storyId: Int32, reaction: Api.Reaction) case updateServiceNotification(flags: Int32, inboxDate: Int32?, type: String, message: String, media: Api.MessageMedia, entities: [Api.MessageEntity]) case updateSmsJob(jobId: String) @@ -2198,6 +2199,12 @@ public extension Api { buffer.appendInt32(1960361625) } + break + case .updateSentPhoneCode(let sentCode): + if boxed { + buffer.appendInt32(1347068303) + } + sentCode.serialize(buffer, true) break case .updateSentStoryReaction(let peer, let storyId, let reaction): if boxed { @@ -2602,6 +2609,8 @@ public extension Api { return ("updateSavedReactionTags", []) case .updateSavedRingtones: return ("updateSavedRingtones", []) + case .updateSentPhoneCode(let sentCode): + return ("updateSentPhoneCode", [("sentCode", sentCode as Any)]) case .updateSentStoryReaction(let peer, let storyId, let reaction): return ("updateSentStoryReaction", [("peer", peer as Any), ("storyId", storyId as Any), ("reaction", reaction as Any)]) case .updateServiceNotification(let flags, let inboxDate, let type, let message, let media, let entities): @@ -4803,6 +4812,19 @@ public extension Api { public static func parse_updateSavedRingtones(_ reader: BufferReader) -> Update? { return Api.Update.updateSavedRingtones } + public static func parse_updateSentPhoneCode(_ reader: BufferReader) -> Update? { + var _1: Api.auth.SentCode? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.auth.SentCode + } + let _c1 = _1 != nil + if _c1 { + return Api.Update.updateSentPhoneCode(sentCode: _1!) + } + else { + return nil + } + } public static func parse_updateSentStoryReaction(_ reader: BufferReader) -> Update? { var _1: Api.Peer? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index a63853f7e2..a223f6f7dc 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -567,6 +567,7 @@ public extension Api.auth { public extension Api.auth { enum SentCode: TypeConstructorDescription { case sentCode(flags: Int32, type: Api.auth.SentCodeType, phoneCodeHash: String, nextType: Api.auth.CodeType?, timeout: Int32?) + case sentCodePaymentRequired(storeProduct: String) case sentCodeSuccess(authorization: Api.auth.Authorization) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -581,6 +582,12 @@ public extension Api.auth { if Int(flags) & Int(1 << 1) != 0 {nextType!.serialize(buffer, true)} if Int(flags) & Int(1 << 2) != 0 {serializeInt32(timeout!, buffer: buffer, boxed: false)} break + case .sentCodePaymentRequired(let storeProduct): + if boxed { + buffer.appendInt32(304435204) + } + serializeString(storeProduct, buffer: buffer, boxed: false) + break case .sentCodeSuccess(let authorization): if boxed { buffer.appendInt32(596704836) @@ -594,6 +601,8 @@ public extension Api.auth { switch self { case .sentCode(let flags, let type, let phoneCodeHash, let nextType, let timeout): return ("sentCode", [("flags", flags as Any), ("type", type as Any), ("phoneCodeHash", phoneCodeHash as Any), ("nextType", nextType as Any), ("timeout", timeout as Any)]) + case .sentCodePaymentRequired(let storeProduct): + return ("sentCodePaymentRequired", [("storeProduct", storeProduct as Any)]) case .sentCodeSuccess(let authorization): return ("sentCodeSuccess", [("authorization", authorization as Any)]) } @@ -626,6 +635,17 @@ public extension Api.auth { return nil } } + public static func parse_sentCodePaymentRequired(_ reader: BufferReader) -> SentCode? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.auth.SentCode.sentCodePaymentRequired(storeProduct: _1!) + } + else { + return nil + } + } public static func parse_sentCodeSuccess(_ reader: BufferReader) -> SentCode? { var _1: Api.auth.Authorization? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index 57f473f326..b6ec88ab5b 100644 --- a/submodules/TelegramApi/Sources/Api3.swift +++ b/submodules/TelegramApi/Sources/Api3.swift @@ -54,6 +54,42 @@ public extension Api { } } +public extension Api { + enum BusinessBotRights: TypeConstructorDescription { + case businessBotRights(flags: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .businessBotRights(let flags): + if boxed { + buffer.appendInt32(-1604170505) + } + serializeInt32(flags, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .businessBotRights(let flags): + return ("businessBotRights", [("flags", flags as Any)]) + } + } + + public static func parse_businessBotRights(_ reader: BufferReader) -> BusinessBotRights? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.BusinessBotRights.businessBotRights(flags: _1!) + } + else { + return nil + } + } + + } +} public extension Api { enum BusinessChatLink: TypeConstructorDescription { case businessChatLink(flags: Int32, link: String, message: String, entities: [Api.MessageEntity]?, title: String?, views: Int32) diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index 9c10a1bc0b..e2bf3f4406 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -1604,13 +1604,14 @@ public extension Api.functions.account { } } public extension Api.functions.account { - static func updateConnectedBot(flags: Int32, bot: Api.InputUser, recipients: Api.InputBusinessBotRecipients) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func updateConnectedBot(flags: Int32, rights: Api.BusinessBotRights?, bot: Api.InputUser, recipients: Api.InputBusinessBotRecipients) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1138250269) + buffer.appendInt32(1721797758) serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {rights!.serialize(buffer, true)} bot.serialize(buffer, true) recipients.serialize(buffer, true) - return (FunctionDescription(name: "account.updateConnectedBot", parameters: [("flags", String(describing: flags)), ("bot", String(describing: bot)), ("recipients", String(describing: recipients))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + return (FunctionDescription(name: "account.updateConnectedBot", parameters: [("flags", String(describing: flags)), ("rights", String(describing: rights)), ("bot", String(describing: bot)), ("recipients", String(describing: recipients))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -9063,11 +9064,11 @@ public extension Api.functions.payments { } } public extension Api.functions.payments { - static func canPurchasePremium(purpose: Api.InputStorePaymentPurpose) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func canPurchaseStore(purpose: Api.InputStorePaymentPurpose) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1614700874) + buffer.appendInt32(1339842215) purpose.serialize(buffer, true) - return (FunctionDescription(name: "payments.canPurchasePremium", parameters: [("purpose", String(describing: purpose))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + return (FunctionDescription(name: "payments.canPurchaseStore", parameters: [("purpose", String(describing: purpose))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in let reader = BufferReader(buffer) var result: Api.Bool? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api5.swift b/submodules/TelegramApi/Sources/Api5.swift index 609de6bbae..2f4236c0b7 100644 --- a/submodules/TelegramApi/Sources/Api5.swift +++ b/submodules/TelegramApi/Sources/Api5.swift @@ -664,25 +664,26 @@ public extension Api { } public extension Api { enum ConnectedBot: TypeConstructorDescription { - case connectedBot(flags: Int32, botId: Int64, recipients: Api.BusinessBotRecipients) + case connectedBot(flags: Int32, botId: Int64, recipients: Api.BusinessBotRecipients, rights: Api.BusinessBotRights) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .connectedBot(let flags, let botId, let recipients): + case .connectedBot(let flags, let botId, let recipients, let rights): if boxed { - buffer.appendInt32(-1123645951) + buffer.appendInt32(-849058964) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(botId, buffer: buffer, boxed: false) recipients.serialize(buffer, true) + rights.serialize(buffer, true) break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .connectedBot(let flags, let botId, let recipients): - return ("connectedBot", [("flags", flags as Any), ("botId", botId as Any), ("recipients", recipients as Any)]) + case .connectedBot(let flags, let botId, let recipients, let rights): + return ("connectedBot", [("flags", flags as Any), ("botId", botId as Any), ("recipients", recipients as Any), ("rights", rights as Any)]) } } @@ -695,11 +696,16 @@ public extension Api { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.BusinessBotRecipients } + var _4: Api.BusinessBotRights? + if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.BusinessBotRights + } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.ConnectedBot.connectedBot(flags: _1!, botId: _2!, recipients: _3!) + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.ConnectedBot.connectedBot(flags: _1!, botId: _2!, recipients: _3!, rights: _4!) } else { return nil diff --git a/submodules/TelegramCore/Sources/Authorization.swift b/submodules/TelegramCore/Sources/Authorization.swift index bd12468878..40a363f57c 100644 --- a/submodules/TelegramCore/Sources/Authorization.swift +++ b/submodules/TelegramCore/Sources/Authorization.swift @@ -356,6 +356,9 @@ public func sendAuthorizationCode(accountManager: AccountManager UInt { - return 200 + return 201 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift index 9bab8302f8..947985dc24 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift @@ -139,6 +139,7 @@ private enum UnauthorizedAccountStateContentsValue: Int32 { case signUp = 5 case passwordRecovery = 6 case awaitingAccountReset = 7 + case payment = 8 } public struct UnauthorizedAccountTermsOfService: PostboxCoding, Equatable { @@ -181,6 +182,7 @@ public indirect enum UnauthorizedAccountStateContents: PostboxCoding, Equatable case passwordRecovery(hint: String, number: String?, code: AuthorizationCode?, emailPattern: String, syncContacts: Bool) case awaitingAccountReset(protectedUntil: Int32, number: String?, syncContacts: Bool) case signUp(number: String, codeHash: String, firstName: String, lastName: String, termsOfService: UnauthorizedAccountTermsOfService?, syncContacts: Bool) + case payment(number: String, codeHash: String, storeProduct: String) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("v", orElse: 0) { @@ -214,6 +216,9 @@ public indirect enum UnauthorizedAccountStateContents: PostboxCoding, Equatable self = .awaitingAccountReset(protectedUntil: decoder.decodeInt32ForKey("protectedUntil", orElse: 0), number: decoder.decodeOptionalStringForKey("number"), syncContacts: decoder.decodeInt32ForKey("syncContacts", orElse: 1) != 0) case UnauthorizedAccountStateContentsValue.signUp.rawValue: self = .signUp(number: decoder.decodeStringForKey("n", orElse: ""), codeHash: decoder.decodeStringForKey("h", orElse: ""), firstName: decoder.decodeStringForKey("f", orElse: ""), lastName: decoder.decodeStringForKey("l", orElse: ""), termsOfService: decoder.decodeObjectForKey("tos", decoder: { UnauthorizedAccountTermsOfService(decoder: $0) }) as? UnauthorizedAccountTermsOfService, syncContacts: decoder.decodeInt32ForKey("syncContacts", orElse: 1) != 0) + + case UnauthorizedAccountStateContentsValue.payment.rawValue: + self = .payment(number: decoder.decodeStringForKey("n", orElse: ""), codeHash: decoder.decodeStringForKey("h", orElse: ""), storeProduct: decoder.decodeStringForKey("storeProduct", orElse: "")) default: assertionFailure() self = .empty @@ -303,6 +308,11 @@ public indirect enum UnauthorizedAccountStateContents: PostboxCoding, Equatable encoder.encodeNil(forKey: "tos") } encoder.encodeInt32(syncContacts ? 1 : 0, forKey: "syncContacts") + case let .payment(number, codeHash, storeProduct): + encoder.encodeInt32(UnauthorizedAccountStateContentsValue.payment.rawValue, forKey: "v") + encoder.encodeString(number, forKey: "n") + encoder.encodeString(codeHash, forKey: "h") + encoder.encodeString(storeProduct, forKey: "storeProduct") } } @@ -374,6 +384,12 @@ public indirect enum UnauthorizedAccountStateContents: PostboxCoding, Equatable } else { return false } + case let .payment(number, codeHash, storeProduct): + if case .payment(number, codeHash, storeProduct) = rhs { + return true + } else { + return false + } } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/ChangeAccountPhoneNumber.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/ChangeAccountPhoneNumber.swift index 7666b030d4..537d05ee5e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/ChangeAccountPhoneNumber.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/ChangeAccountPhoneNumber.swift @@ -113,7 +113,7 @@ func _internal_requestChangeAccountPhoneNumberVerification(account: Account, api } else { return .single(ChangeAccountPhoneNumberData(type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType)) } - case .sentCodeSuccess: + case .sentCodeSuccess, .sentCodePaymentRequired: return .never() } } @@ -188,7 +188,7 @@ private func internalResendChangeAccountPhoneNumberVerification(account: Account } else { return .single(ChangeAccountPhoneNumberData(type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType)) } - case .sentCodeSuccess: + case .sentCodeSuccess, .sentCodePaymentRequired: return .never() } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Auth/CancelAccountReset.swift b/submodules/TelegramCore/Sources/TelegramEngine/Auth/CancelAccountReset.swift index b0bd632e63..b1dc5b9705 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Auth/CancelAccountReset.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Auth/CancelAccountReset.swift @@ -34,7 +34,7 @@ func _internal_requestCancelAccountResetData(network: Network, hash: String) -> parsedNextType = AuthorizationCodeNextType(apiType: nextType) } return .single(CancelAccountResetData(type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: timeout, nextType: parsedNextType)) - case .sentCodeSuccess: + case .sentCodeSuccess, .sentCodePaymentRequired: return .never() } } @@ -57,7 +57,7 @@ func _internal_requestNextCancelAccountResetOption(network: Network, phoneNumber parsedNextType = AuthorizationCodeNextType(apiType: nextType) } return .single(CancelAccountResetData(type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: timeout, nextType: parsedNextType)) - case .sentCodeSuccess: + case .sentCodeSuccess, .sentCodePaymentRequired: return .never() } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/ItemCache/TelegramEngineItemCache.swift b/submodules/TelegramCore/Sources/TelegramEngine/ItemCache/TelegramEngineItemCache.swift index 8723e0ec22..a3ed8f1092 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/ItemCache/TelegramEngineItemCache.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/ItemCache/TelegramEngineItemCache.swift @@ -38,3 +38,44 @@ public extension TelegramEngine { } } } + +public extension TelegramEngineUnauthorized { + final class ItemCache { + private let account: UnauthorizedAccount + + init(account: UnauthorizedAccount) { + self.account = account + } + + public func put(collectionId: Int8, id: EngineDataBuffer, item: T) -> Signal { + return self.account.postbox.transaction { transaction -> Void in + if let entry = CodableEntry(item) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: collectionId, key: id), entry: entry) + } + } + |> ignoreValues + } + + public func get(collectionId: Int8, id: EngineDataBuffer) -> Signal { + return self.account.postbox.transaction { transaction -> CodableEntry? in + return transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: collectionId, key: id)) + } + } + + public func remove(collectionId: Int8, id: EngineDataBuffer) -> Signal { + return self.account.postbox.transaction { transaction -> Void in + transaction.removeItemCacheEntry(id: ItemCacheEntryId(collectionId: collectionId, key: id)) + } + |> ignoreValues + } + + public func clear(collectionIds: [Int8]) -> Signal { + return self.account.postbox.transaction { transaction -> Void in + for id in collectionIds { + transaction.clearItemCacheCollection(collectionId: id) + } + } + |> ignoreValues + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift index f282a1ad39..4c7c5b1471 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift @@ -1072,7 +1072,7 @@ public func _internal_setAccountConnectedBot(account: Account, bot: TelegramAcco flags |= 1 << 1 } - return account.network.request(Api.functions.account.updateConnectedBot(flags: flags, bot: mappedBot, recipients: mappedRecipients)) + return account.network.request(Api.functions.account.updateConnectedBot(flags: flags, rights: nil, bot: mappedBot, recipients: mappedRecipients)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift index b30434af1b..68f0cf550a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift @@ -20,9 +20,10 @@ public enum AppStoreTransactionPurpose { case stars(count: Int64, currency: String, amount: Int64) case starsGift(peerId: EnginePeer.Id, count: Int64, currency: String, amount: Int64) case starsGiveaway(stars: Int64, boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64, users: Int32) + case authCode(restore: Bool, phoneNumber: String, phoneCodeHash: String, currency: String, amount: Int64) } -private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTransactionPurpose) -> Signal { +private func apiInputStorePaymentPurpose(postbox: Postbox, purpose: AppStoreTransactionPurpose) -> Signal { switch purpose { case .subscription, .upgrade, .restore: var flags: Int32 = 0 @@ -36,7 +37,7 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran } return .single(.inputStorePaymentPremiumSubscription(flags: flags)) case let .gift(peerId, currency, amount): - return account.postbox.loadedPeerWithId(peerId) + return postbox.loadedPeerWithId(peerId) |> mapToSignal { peer -> Signal in guard let inputUser = apiInputUser(peer) else { return .complete() @@ -44,7 +45,7 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran return .single(.inputStorePaymentGiftPremium(userId: inputUser, currency: currency, amount: amount)) } case let .giftCode(peerIds, boostPeerId, currency, amount, text, entities): - return account.postbox.transaction { transaction -> Api.InputStorePaymentPurpose in + return postbox.transaction { transaction -> Api.InputStorePaymentPurpose in var flags: Int32 = 0 var apiBoostPeer: Api.InputPeer? var apiInputUsers: [Api.InputUser] = [] @@ -69,7 +70,7 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran return .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: currency, amount: amount, message: message) } case let .giveaway(boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, currency, amount): - return account.postbox.transaction { transaction -> Signal in + return postbox.transaction { transaction -> Signal in guard let peer = transaction.getPeer(boostPeerId), let apiBoostPeer = apiInputPeer(peer) else { return .complete() } @@ -101,7 +102,7 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran case let .stars(count, currency, amount): return .single(.inputStorePaymentStarsTopup(stars: count, currency: currency, amount: amount)) case let .starsGift(peerId, count, currency, amount): - return account.postbox.loadedPeerWithId(peerId) + return postbox.loadedPeerWithId(peerId) |> mapToSignal { peer -> Signal in guard let inputUser = apiInputUser(peer) else { return .complete() @@ -109,7 +110,7 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran return .single(.inputStorePaymentStarsGift(userId: inputUser, stars: count, currency: currency, amount: amount)) } case let .starsGiveaway(stars, boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, currency, amount, users): - return account.postbox.transaction { transaction -> Signal in + return postbox.transaction { transaction -> Signal in guard let peer = transaction.getPeer(boostPeerId), let apiBoostPeer = apiInputPeer(peer) else { return .complete() } @@ -138,14 +139,20 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran return .single(.inputStorePaymentStarsGiveaway(flags: flags, stars: stars, boostPeer: apiBoostPeer, additionalPeers: additionalPeers, countriesIso2: countries, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount, users: users)) } |> switchToLatest + case let .authCode(restore, phoneNumber, phoneCodeHash, currency, amount): + var flags: Int32 = 0 + if restore { + flags |= (1 << 0) + } + return .single(.inputStorePaymentAuthCode(flags: flags, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, currency: currency, amount: amount)) } } -func _internal_sendAppStoreReceipt(account: Account, receipt: Data, purpose: AppStoreTransactionPurpose) -> Signal { - return apiInputStorePaymentPurpose(account: account, purpose: purpose) +func _internal_sendAppStoreReceipt(postbox: Postbox, network: Network, stateManager: AccountStateManager?, receipt: Data, purpose: AppStoreTransactionPurpose) -> Signal { + return apiInputStorePaymentPurpose(postbox: postbox, purpose: purpose) |> castError(AssignAppStoreTransactionError.self) |> mapToSignal { purpose -> Signal in - return account.network.request(Api.functions.payments.assignAppStoreTransaction(receipt: Buffer(data: receipt), purpose: purpose)) + return network.request(Api.functions.payments.assignAppStoreTransaction(receipt: Buffer(data: receipt), purpose: purpose)) |> mapError { error -> AssignAppStoreTransactionError in if error.errorCode == 406 { return .serverProvided @@ -154,7 +161,7 @@ func _internal_sendAppStoreReceipt(account: Account, receipt: Data, purpose: App } } |> mapToSignal { updates -> Signal in - account.stateManager.addUpdates(updates) + stateManager?.addUpdates(updates) return .complete() } } @@ -164,10 +171,10 @@ public enum RestoreAppStoreReceiptError { case generic } -func _internal_canPurchasePremium(account: Account, purpose: AppStoreTransactionPurpose) -> Signal { - return apiInputStorePaymentPurpose(account: account, purpose: purpose) +func _internal_canPurchasePremium(postbox: Postbox, network: Network, purpose: AppStoreTransactionPurpose) -> Signal { + return apiInputStorePaymentPurpose(postbox: postbox, purpose: purpose) |> mapToSignal { purpose -> Signal in - return account.network.request(Api.functions.payments.canPurchasePremium(purpose: purpose)) + return network.request(Api.functions.payments.canPurchaseStore(purpose: purpose)) |> map { result -> Bool in switch result { case .boolTrue: diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 2587b5fc2b..e4722af322 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -568,6 +568,21 @@ private final class StarsContextImpl { self._state = state self._statePromise.set(.single(state)) } + + var onUpdate: Signal { + return self._statePromise.get() + |> take(until: { value in + if let value { + if !value.flags.contains(.isPendingBalance) { + return SignalTakeAction(passthrough: true, complete: true) + } + } + return SignalTakeAction(passthrough: false, complete: false) + }) + |> map { _ in + return Void() + } + } } private extension StarsContext.State.Transaction { @@ -1011,6 +1026,18 @@ public final class StarsContext { } } + public var onUpdate: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.onUpdate.start(next: { value in + subscriber.putNext(value) + })) + } + return disposable + } + } + init(account: Account) { self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 0f6f152b40..c26d11bc60 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -39,11 +39,11 @@ public extension TelegramEngine { } public func sendAppStoreReceipt(receipt: Data, purpose: AppStoreTransactionPurpose) -> Signal { - return _internal_sendAppStoreReceipt(account: self.account, receipt: receipt, purpose: purpose) + return _internal_sendAppStoreReceipt(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, receipt: receipt, purpose: purpose) } public func canPurchasePremium(purpose: AppStoreTransactionPurpose) -> Signal { - return _internal_canPurchasePremium(account: self.account, purpose: purpose) + return _internal_canPurchasePremium(postbox: self.account.postbox, network: self.account.network, purpose: purpose) } public func checkPremiumGiftCode(slug: String) -> Signal { @@ -150,3 +150,21 @@ public extension TelegramEngine { } } } + +public extension TelegramEngineUnauthorized { + final class Payments { + private let account: UnauthorizedAccount + + init(account: UnauthorizedAccount) { + self.account = account + } + + public func canPurchasePremium(purpose: AppStoreTransactionPurpose) -> Signal { + return _internal_canPurchasePremium(postbox: self.account.postbox, network: self.account.network, purpose: purpose) + } + + public func sendAppStoreReceipt(receipt: Data, purpose: AppStoreTransactionPurpose) -> Signal { + return _internal_sendAppStoreReceipt(postbox: self.account.postbox, network: self.account.network, stateManager: nil, receipt: receipt, purpose: purpose) + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 9c68e06e69..4212a15e8e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -246,7 +246,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee if let apiBot = connectedBots.first { switch apiBot { - case let .connectedBot(flags, botId, recipients): + case let .connectedBot(flags, botId, recipients, _): mappedConnectedBot = TelegramAccountConnectedBot( id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), recipients: TelegramBusinessRecipients(apiValue: recipients), diff --git a/submodules/TelegramCore/Sources/TelegramEngine/SecureId/VerifySecureIdValue.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/VerifySecureIdValue.swift index 7f0428cc04..400feb5289 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/SecureId/VerifySecureIdValue.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/VerifySecureIdValue.swift @@ -30,7 +30,7 @@ public func secureIdPreparePhoneVerification(network: Network, value: SecureIdPh switch sentCode { case let .sentCode(_, type, phoneCodeHash, nextType, timeout): return .single(SecureIdPreparePhoneVerificationPayload(type: SentAuthorizationCodeType(apiType: type), nextType: nextType.flatMap(AuthorizationCodeNextType.init), timeout: timeout, phone: value.phone, phoneCodeHash: phoneCodeHash)) - case .sentCodeSuccess: + case .sentCodeSuccess, .sentCodePaymentRequired: return .never() } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/TelegramEngine.swift b/submodules/TelegramCore/Sources/TelegramEngine/TelegramEngine.swift index 05a80077d9..35a49e9ea3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/TelegramEngine.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/TelegramEngine.swift @@ -107,6 +107,14 @@ public final class TelegramEngineUnauthorized { public lazy var localization: Localization = { return Localization(account: self.account) }() + + public lazy var payments: Payments = { + return Payments(account: self.account) + }() + + public lazy var itemCache: ItemCache = { + return ItemCache(account: self.account) + }() } public enum SomeTelegramEngine { diff --git a/submodules/TelegramStringFormatting/Sources/TonFormat.swift b/submodules/TelegramStringFormatting/Sources/TonFormat.swift index de64f93e82..c237580f34 100644 --- a/submodules/TelegramStringFormatting/Sources/TonFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/TonFormat.swift @@ -81,7 +81,7 @@ public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDate public func formatStarsAmountText(_ amount: StarsAmount, dateTimeFormat: PresentationDateTimeFormat, showPlus: Bool = false) -> String { var balanceText = presentationStringsFormattedNumber(Int32(amount.value), dateTimeFormat.groupingSeparator) - let fraction = Double(amount.nanos) / 10e6 + let fraction = abs(Double(amount.nanos)) / 10e6 if fraction > 0.0 { balanceText.append(dateTimeFormat.decimalSeparator) balanceText.append("\(Int32(fraction))") diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index ce5fdeaf86..4238867181 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -466,6 +466,7 @@ swift_library( "//submodules/TelegramUI/Components/ContentReportScreen", "//submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen", "//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent", + "//submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen", "//third-party/recaptcha:RecaptchaEnterprise", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift b/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift index 7ad32eb899..65f23993ee 100644 --- a/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift +++ b/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift @@ -193,6 +193,7 @@ private final class ScrollContent: CombinedComponent { adsText = strings.AdsInfo_Bot_Ads_Text infoRawText = strings.AdsInfo_Bot_Launch_Text case .search: + //TODO:localize respectText = "Ads like this do not use your personal information and are based on the search query you entered." adsText = strings.AdsInfo_Bot_Ads_Text infoRawText = "Anyone can create an ad to display in search results for any query. Check out the Telegram Ad Platform for details. [Learn More >]()" diff --git a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift index b63ae30563..be901426e8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift @@ -1787,7 +1787,7 @@ public final class ChatEmptyNode: ASDisplayNode { isScheduledMessages = true } - let contentType: ChatEmptyNodeContentType + var contentType: ChatEmptyNodeContentType var displayAttachedDescription = false switch subject { case .detailsPlaceholder: @@ -1815,7 +1815,7 @@ public final class ChatEmptyNode: ASDisplayNode { } else if let _ = interfaceState.peerNearbyData { contentType = .peerNearby } else if let peer = peer as? TelegramUser { - if let _ = interfaceState.sendPaidMessageStars { + if let _ = interfaceState.sendPaidMessageStars, interfaceState.businessIntro == nil { contentType = .starsRequired } else if interfaceState.isPremiumRequiredForMessaging { contentType = .premiumRequired diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift index e45c8961d1..36ef1d2e41 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift @@ -687,6 +687,8 @@ public final class ChatInlineSearchResultsListComponent: Component { openPhotoSetup: { }, openAdInfo: { _ in + }, + openAccountFreezeInfo: { } ) self.chatListNodeInteraction = chatListNodeInteraction diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index ffa7a26e05..eee6a16b63 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -299,7 +299,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } if isMediaInverted { - result.insert((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default)), at: 0) + result.insert((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default)), at: addedPriceInfo ? 1 : 0) } else { result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default))) needReactions = false @@ -327,7 +327,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } if let attribute = message.attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute, attribute.leadingPreview { - result.insert((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)), at: 0) + result.insert((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)), at: addedPriceInfo ? 1 : 0) } else { result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } @@ -362,7 +362,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ if result.isEmpty { needReactions = false } - result.insert((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)), at: 0) + result.insert((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)), at: addedPriceInfo ? 1 : 0) } else { result.append((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false diff --git a/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift index 9bf11f830f..c7beb06383 100644 --- a/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift @@ -393,7 +393,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe let disclaimerText: NSMutableAttributedString if let verification = item.verification { - disclaimerText = NSMutableAttributedString(string: " # \(verification.description)", font: Font.regular(13.0), textColor: subtitleColor) + disclaimerText = NSMutableAttributedString(string: " # \(verification.description)", font: Font.regular(13.0), textColor: subtitleColor) if let range = disclaimerText.string.range(of: "#") { disclaimerText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: verification.iconFileId, file: nil), range: NSRange(range, in: disclaimerText.string)) disclaimerText.addAttribute(.foregroundColor, value: subtitleColor, range: NSRange(range, in: disclaimerText.string)) diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index b684b61345..5f3287cf6f 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -1237,6 +1237,11 @@ final class GiftOptionsScreenComponent: Component { if availableProducts.isEmpty { var premiumProducts: [PremiumGiftProduct] = [] for option in premiumOptions { + if option.currency == "XTR" { + continue + } + let starsGiftOption = premiumOptions.first(where: { $0.currency == "XTR" && $0.months == option.months }) + premiumProducts.append( PremiumGiftProduct( giftOption: CachedPremiumGiftOption( @@ -1246,7 +1251,7 @@ final class GiftOptionsScreenComponent: Component { botUrl: "", storeProductId: option.storeProductId ), - starsGiftOption: nil, + starsGiftOption: starsGiftOption, storeProduct: nil, discount: nil ) diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 56549057c4..ece1fa0f97 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -498,16 +498,8 @@ final class GiftSetupScreenComponent: Component { starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) - let _ = (starsContext.state - |> take(until: { value in - if let value { - if !value.flags.contains(.isPendingBalance) { - return SignalTakeAction(passthrough: true, complete: true) - } - } - return SignalTakeAction(passthrough: false, complete: false) - }) - |> deliverOnMainQueue).start(next: { _ in + let _ = (starsContext.onUpdate + |> deliverOnMainQueue).start(next: { proceed() }) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 1ca31db8c7..d1ecd50d04 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -383,10 +383,14 @@ private final class GiftViewSheetContent: CombinedComponent { options: options ?? [], purpose: .upgradeStarGift(requiredStars: price), completion: { [weak starsContext] stars in - starsContext?.add(balance: StarsAmount(value: stars, nanos: 0)) - Queue.mainQueue().after(2.0) { - proceed(upgradeForm.id) + guard let starsContext else { + return } + starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) + let _ = (starsContext.onUpdate + |> deliverOnMainQueue).start(next: { + proceed(upgradeForm.id) + }) } ) controller.push(purchaseController) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift index 787b697927..4816daf1b6 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift @@ -188,6 +188,7 @@ public final class LoadingOverlayNode: ASDisplayNode { }, openWebApp: { _ in }, openPhotoSetup: { }, openAdInfo: { _ in + }, openAccountFreezeInfo: { }) let items = (0 ..< 1).map { _ -> ChatListItem in @@ -548,6 +549,8 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod openPhotoSetup: { }, openAdInfo: { _ in + }, + openAccountFreezeInfo: { } ) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index e924b19dd6..a8e93576fc 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -998,7 +998,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr }))) } - if canReorder { + if case .unique = gift.gift, canReorder { items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Context_Reorder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in c?.dismiss(completion: { [weak self] in guard let self else { diff --git a/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/BUILD b/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/BUILD new file mode 100644 index 0000000000..0f7c19aaa1 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/BUILD @@ -0,0 +1,36 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AccountFreezeInfoScreen", + module_name = "AccountFreezeInfoScreen", + 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/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/Components/SheetComponent", + "//submodules/PresentationDataUtils", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/Markdown", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/Sources/AccountFreezeInfoScreen.swift b/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/Sources/AccountFreezeInfoScreen.swift new file mode 100644 index 0000000000..64960bfc00 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/Sources/AccountFreezeInfoScreen.swift @@ -0,0 +1,578 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import Markdown +import TextFormat +import TelegramPresentationData +import TelegramStringFormatting +import ViewControllerComponent +import SheetComponent +import BundleIconComponent +import BalancedTextComponent +import MultilineTextComponent +import LottieComponent +import ButtonComponent +import AccountContext + +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let configuration: AccountFreezeConfiguration + let submitAppeal: () -> Void + let dismiss: () -> Void + + init( + context: AccountContext, + configuration: AccountFreezeConfiguration, + submitAppeal: @escaping () -> Void, + dismiss: @escaping () -> Void + ) { + self.context = context + self.configuration = configuration + self.submitAppeal = submitAppeal + self.dismiss = dismiss + } + + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class State: ComponentState { + var cachedChevronImage: (UIImage, PresentationTheme)? + var cachedCloseImage: (UIImage, PresentationTheme)? + + let playOnce = ActionSlot() + private var didPlayAnimation = false + + func playAnimationIfNeeded() { + guard !self.didPlayAnimation else { + return + } + self.didPlayAnimation = true + self.playOnce.invoke(Void()) + } + } + + func makeState() -> State { + return State() + } + + static var body: Body { + let animation = Child(LottieComponent.self) + + let title = Child(BalancedTextComponent.self) + let list = Child(List.self) + let actionButton = Child(ButtonComponent.self) + let closeButton = Child(ButtonComponent.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let component = context.component + let state = context.state + + let theme = environment.theme + let strings = environment.strings + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let textSideInset: CGFloat = 30.0 + environment.safeInsets.left + + let titleFont = Font.semibold(20.0) + + let textColor = theme.actionSheet.primaryTextColor + let secondaryTextColor = theme.actionSheet.secondaryTextColor + let linkColor = theme.actionSheet.controlAccentColor + + let spacing: CGFloat = 16.0 + var contentSize = CGSize(width: context.availableSize.width, height: 32.0) + + let animationHeight: CGFloat = 120.0 + let animation = animation.update( + component: LottieComponent( + content: LottieComponent.AppBundleContent(name: "Banned"), + startingPosition: .begin, + playOnce: state.playOnce + ), + environment: {}, + availableSize: CGSize(width: animationHeight, height: animationHeight), + transition: .immediate + ) + context.add(animation + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + animation.size.height / 2.0)) + ) + contentSize.height += animation.size.height + contentSize.height += spacing + 5.0 + + let title = title.update( + component: BalancedTextComponent( + text: .plain(NSAttributedString(string: "Your Account is Frozen", font: titleFont, textColor: textColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.1 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) + ) + contentSize.height += title.size.height + contentSize.height += spacing - 2.0 + + //TODO:localize + var items: [AnyComponentWithIdentity] = [] + items.append( + AnyComponentWithIdentity( + id: "ads", + component: AnyComponent(ParagraphComponent( + title: "Violation of Terms", + titleColor: textColor, + text: "Your account was frozen for breaking Telegram's Terms and Conditions.", + textColor: secondaryTextColor, + iconName: "Account Freeze/Violation", + iconColor: linkColor + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "split", + component: AnyComponent(ParagraphComponent( + title: "Read-Only Mode", + titleColor: textColor, + text: "You can access your account but can't send messages or take actions.", + textColor: secondaryTextColor, + iconName: "Ads/Privacy", + iconColor: linkColor + )) + ) + ) + let dateString = stringForFullDate(timestamp: component.configuration.freezeUntilDate ?? 0, strings: strings, dateTimeFormat: environment.dateTimeFormat) + items.append( + AnyComponentWithIdentity( + id: "withdrawal", + component: AnyComponent(ParagraphComponent( + title: "Appeal Before Deactivation", + titleColor: textColor, + text: "Appeal via [@SpamBot]() before \(dateString), or your account will be deleted.", + textColor: secondaryTextColor, + iconName: "Account Freeze/Appeal", + iconColor: linkColor, + action: { + component.submitAppeal() + Queue.mainQueue().after(1.0) { + component.dismiss() + } + } + )) + ) + ) + + let list = list.update( + component: List(items), + availableSize: CGSize(width: context.availableSize.width - sideInset, height: 10000.0), + transition: context.transition + ) + context.add(list + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0)) + ) + contentSize.height += list.size.height + contentSize.height += spacing + 2.0 + + let buttonAttributedString = NSMutableAttributedString(string: "Submit an Appeal", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + let actionButton = actionButton.update( + component: ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 10.0 + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) + ), + isEnabled: true, + displaysProgress: false, + action: { + component.submitAppeal() + Queue.mainQueue().after(1.0) { + component.dismiss() + } + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + context.add(actionButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0)) + .cornerRadius(10.0) + ) + contentSize.height += actionButton.size.height + contentSize.height += 8.0 + + let closeAttributedString = NSMutableAttributedString(string: "Understood", font: Font.regular(17.0), textColor: environment.theme.list.itemCheckColors.fillColor, paragraphAlignment: .center) + let closeButton = closeButton.update( + component: ButtonComponent( + background: ButtonComponent.Background( + color: .clear, + foreground: .clear, + pressedColor: .clear, + cornerRadius: 10.0 + ), + content: AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(MultilineTextComponent(text: .plain(closeAttributedString))) + ), + isEnabled: true, + displaysProgress: false, + action: { + component.dismiss() + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + context.add(closeButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0)) + ) + contentSize.height += closeButton.size.height + + if environment.safeInsets.bottom > 0 { + contentSize.height += environment.safeInsets.bottom + 5.0 + } else { + contentSize.height += 12.0 + } + + state.playAnimationIfNeeded() + + return contentSize + } + } +} + +private final class SheetContainerComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let configuration: AccountFreezeConfiguration + let submitAppeal: () -> Void + + init( + context: AccountContext, + configuration: AccountFreezeConfiguration, + submitAppeal: @escaping () -> Void + ) { + self.context = context + self.configuration = configuration + self.submitAppeal = submitAppeal + } + + static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + let sheetExternalState = SheetComponent.ExternalState() + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(SheetContent( + context: context.component.context, + configuration: context.component.configuration, + submitAppeal: context.component.submitAppeal, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } + )), + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + followContentSizeChanges: true, + externalState: sheetExternalState, + animateOut: animateOut + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + if let controller = controller(), !controller.automaticallyControlPresentationContextLayout { + let layout = ContainerViewLayout( + size: context.availableSize, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0), + safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right), + additionalInsets: .zero, + statusBarHeight: environment.statusBarHeight, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition) + } + + return context.availableSize + } + } +} + + +public final class AccountFreezeInfoScreen: ViewControllerComponentContainer { + private let context: AccountContext + + public init( + context: AccountContext + ) { + self.context = context + + let configuration = AccountFreezeConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + var submitAppealImpl: (() -> Void)? + super.init( + context: context, + component: SheetContainerComponent( + context: context, + configuration: configuration, + submitAppeal: { + submitAppealImpl?() + } + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + + submitAppealImpl = { [weak self] in + guard let self, let url = configuration.freezeAppealUrl else { + return + } + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: false, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: self.navigationController as? NavigationController, dismissInput: {}) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} + +private final class ParagraphComponent: CombinedComponent { + let title: String + let titleColor: UIColor + let text: String + let textColor: UIColor + let iconName: String + let iconColor: UIColor + let action: (() -> Void)? + + public init( + title: String, + titleColor: UIColor, + text: String, + textColor: UIColor, + iconName: String, + iconColor: UIColor, + action: (() -> Void)? = nil + ) { + self.title = title + self.titleColor = titleColor + self.text = text + self.textColor = textColor + self.iconName = iconName + self.iconColor = iconColor + self.action = action + } + + static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.titleColor != rhs.titleColor { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.textColor != rhs.textColor { + return false + } + if lhs.iconName != rhs.iconName { + return false + } + if lhs.iconColor != rhs.iconColor { + return false + } + return true + } + + static var body: Body { + let title = Child(MultilineTextComponent.self) + let text = Child(MultilineTextComponent.self) + let icon = Child(BundleIconComponent.self) + + return { context in + let component = context.component + + let leftInset: CGFloat = 64.0 + let rightInset: CGFloat = 32.0 + let textSideInset: CGFloat = leftInset + 8.0 + let spacing: CGFloat = 5.0 + + let textTopInset: CGFloat = 9.0 + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.title, + font: Font.semibold(15.0), + textColor: component.titleColor, + paragraphAlignment: .natural + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let textColor = component.textColor + let linkColor = component.iconColor + let markdownAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: linkColor), + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + } + ) + + let text = text.update( + component: MultilineTextComponent( + text: .markdown(text: component.text, attributes: markdownAttributes), + horizontalAlignment: .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.2, + highlightColor: linkColor.withAlphaComponent(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { attributes, _ in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + component.action?() + } + } + ), + availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height), + transition: .immediate + ) + + let icon = icon.update( + component: BundleIconComponent( + name: component.iconName, + tintColor: component.iconColor + ), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), + transition: .immediate + ) + + context.add(title + .position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) + ) + + context.add(text + .position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0)) + ) + + context.add(icon + .position(CGPoint(x: 47.0, y: textTopInset + 18.0)) + ) + + return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 18.0) + } + } +} + +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() + }) +} diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift index 8c463011d9..7e0a7745b6 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift @@ -213,6 +213,8 @@ final class GreetingMessageListItemComponent: Component { openPhotoSetup: { }, openAdInfo: { _ in + }, + openAccountFreezeInfo: { } ) self.chatListNodeInteraction = chatListNodeInteraction diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index 277046ce3f..dd19b5efa6 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -234,6 +234,8 @@ final class QuickReplySetupScreenComponent: Component { openPhotoSetup: { }, openAdInfo: { _ in + }, + openAccountFreezeInfo: { } ) diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index 0a35bc76f5..2b419b6569 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -876,6 +876,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate }, openWebApp: { _ in }, openPhotoSetup: { }, openAdInfo: { _ in + }, openAccountFreezeInfo: { }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 46cbbfaf0a..d556d7a17c 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -589,10 +589,15 @@ private final class SheetContent: CombinedComponent { options: state?.options ?? [], purpose: purpose, completion: { [weak starsContext] stars in - starsContext?.add(balance: StarsAmount(value: stars, nanos: 0)) - Queue.mainQueue().after(0.1) { - completion() + guard let starsContext else { + return } + starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) + + let _ = (starsContext.onUpdate + |> deliverOnMainQueue).start(next: { + completion() + }) } ) controller?.push(purchaseController) diff --git a/submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/Contents.json new file mode 100644 index 0000000000..93a1282c31 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "hand_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/hand_30.pdf b/submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/hand_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..27b21b63e4f47493374449cd04585d92bb9f7b83 GIT binary patch literal 4643 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 Y_m68DxI6YoC1j)_QUF0g4Fk>p03>T6r~m)} literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Account Freeze/Contents.json b/submodules/TelegramUI/Images.xcassets/Account Freeze/Contents.json new file mode 100644 index 0000000000..6e965652df --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Account Freeze/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/Contents.json new file mode 100644 index 0000000000..ecd534d108 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "sandtimer_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/sandtimer_30.pdf b/submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/sandtimer_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..740b891fa0ea4f6dfebd4f34014d5ce2d33bf183 GIT binary patch literal 4669 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 P00000NkvXXu0mjf^&h70 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Components/PayMock.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Components/PayMock.imageset/Contents.json new file mode 100644 index 0000000000..0e82572e81 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Components/PayMock.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "MockSMS.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Components/PayMock.imageset/MockSMS.png b/submodules/TelegramUI/Images.xcassets/Components/PayMock.imageset/MockSMS.png new file mode 100644 index 0000000000000000000000000000000000000000..cfb9545fe20a595269bf126eec1b5a63d8b56afa GIT binary patch literal 88276 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

3Z` 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 deliverOnMainQueue).start(next: { value in let _ = currentCountriesConfiguration.swap(CountriesConfiguration(countries: value)) }) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 3913193b89..20be263b74 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -4412,7 +4412,18 @@ extension ChatControllerImpl { }, openMessagePayment: { }, openBoostToUnrestrict: { [weak self] in - guard let self, let peerId = self.chatLocation.peerId, let cachedData = self.peerView?.cachedData as? CachedChannelData, let boostToUnrestrict = cachedData.boostsToUnrestrict else { + guard let self else { + return + } + + let accountFreezeConfiguration = AccountFreezeConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) + if let _ = accountFreezeConfiguration.freezeUntilDate { + let controller = self.context.sharedContext.makeAccountFreezeInfoScreen(context: self.context) + self.push(controller) + return + } + + guard let peerId = self.chatLocation.peerId, let cachedData = self.peerView?.cachedData as? CachedChannelData, let boostToUnrestrict = cachedData.boostsToUnrestrict else { return } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift index cae3d5b3d9..16d06cfde1 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift @@ -72,7 +72,10 @@ extension ChatControllerImpl { return } let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: options, purpose: .sendMessage(peerId: peer.id, requiredStars: totalAmount), completion: { _ in - completion(false) + let _ = (starsContext.onUpdate + |> deliverOnMainQueue).start(next: { + completion(false) + }) }) self.push(controller) }) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index 57468824f2..486f255b1c 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -21,6 +21,18 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState return (nil, nil) } + let accountFreezeConfiguration = AccountFreezeConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + if let _ = accountFreezeConfiguration.freezeUntilDate { + if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { + return (currentPanel, nil) + } else { + let panel = ChatRestrictedInputPanelNode() + panel.context = context + panel.interfaceInteraction = interfaceInteraction + return (panel, nil) + } + } + if let _ = chatPresentationInterfaceState.search { var selectionPanel: ChatMessageSelectionInputPanelNode? if let selectionState = chatPresentationInterfaceState.interfaceState.selectionState { diff --git a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift index 7eac909522..fbc2f26eb0 100644 --- a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift @@ -583,11 +583,7 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { } } - /*#if DEBUG - emojiStatus = PeerEmojiStatus(fileId: 5062172592505356289, expirationDate: nil) - #endif*/ - - if let emojiStatus = emojiStatus { + if let emojiStatus = emojiStatus, case .emoji = emojiStatus.content { self.emojiSeparatorNode.isHidden = false transition.updateFrame(node: self.emojiSeparatorNode, frame: CGRect(origin: CGPoint(x: leftInset + 12.0, y: 40.0), size: CGSize(width: width - leftInset - rightInset - 24.0, height: UIScreenPixel))) diff --git a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift index 0e529f49c8..2f3fe48f6a 100644 --- a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift @@ -9,10 +9,12 @@ import TelegramStringFormatting import ChatPresentationInterfaceState import TelegramPresentationData import ChatInputPanelNode +import AccountContext final class ChatRestrictedInputPanelNode: ChatInputPanelNode { private let buttonNode: HighlightTrackingButtonNode private let textNode: ImmediateTextNode + private let subtitleNode: ImmediateTextNode private var iconView: UIImageView? private var presentationInterfaceState: ChatPresentationInterfaceState? @@ -22,12 +24,17 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { self.textNode.maximumNumberOfLines = 2 self.textNode.textAlignment = .center + self.subtitleNode = ImmediateTextNode() + self.subtitleNode.maximumNumberOfLines = 1 + self.subtitleNode.textAlignment = .center + self.buttonNode = HighlightTrackingButtonNode() self.buttonNode.isUserInteractionEnabled = false super.init() self.addSubnode(self.textNode) + self.addSubnode(self.subtitleNode) self.addSubnode(self.buttonNode) self.buttonNode.highligthedChanged = { [weak self] highlighted in @@ -37,11 +44,15 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { self.iconView?.alpha = 0.4 self.textNode.layer.removeAnimation(forKey: "opacity") self.textNode.alpha = 0.4 + self.subtitleNode.layer.removeAnimation(forKey: "opacity") + self.subtitleNode.alpha = 0.4 } else { self.iconView?.alpha = 1.0 self.iconView?.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) self.textNode.alpha = 1.0 self.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + self.subtitleNode.alpha = 1.0 + self.subtitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } @@ -74,7 +85,16 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { var iconSpacing: CGFloat = 4.0 var isUserInteractionEnabled = false - if case let .replyThread(message) = interfaceState.chatLocation, message.peerId == self.context?.account.peerId { + var accountFreezeConfiguration: AccountFreezeConfiguration? + if let context = self.context { + accountFreezeConfiguration = AccountFreezeConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + } + //TODO:localize + if let _ = accountFreezeConfiguration?.freezeUntilDate { + self.textNode.attributedText = NSAttributedString(string: "You account is frozen", font: Font.semibold(15.0), textColor: interfaceState.theme.list.itemDestructiveColor) + self.subtitleNode.attributedText = NSAttributedString(string: "Tap to view details", font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) + isUserInteractionEnabled = true + } else if case let .replyThread(message) = interfaceState.chatLocation, message.peerId == self.context?.account.peerId { self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Chat_PanelStatusAuthorHidden, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) } else if let threadData = interfaceState.threadData, threadData.isClosed { iconImage = PresentationResourcesChat.chatPanelLockIcon(interfaceState.theme) @@ -116,6 +136,7 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { let panelHeight = defaultHeight(metrics: metrics) let textSize = self.textNode.updateLayout(CGSize(width: width - leftInset - rightInset - 8.0 * 2.0, height: panelHeight)) + let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: width - leftInset - rightInset - 8.0 * 2.0, height: panelHeight)) var originX: CGFloat = leftInset + floor((width - leftInset - rightInset - textSize.width) / 2.0) @@ -139,10 +160,18 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { iconView.removeFromSuperview() } - let textFrame = CGRect(origin: CGPoint(x: originX, y: floor((panelHeight - textSize.height) / 2.0)), size: textSize) + var combinedHeight: CGFloat = textSize.height + if subtitleSize.height > 0.0 { + combinedHeight += subtitleSize.height + 2.0 + } + let textFrame = CGRect(origin: CGPoint(x: originX, y: floor((panelHeight - combinedHeight) / 2.0)), size: textSize) self.textNode.frame = textFrame - self.buttonNode.frame = textFrame.insetBy(dx: -8.0, dy: -12.0) + let subtitleFrame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - subtitleSize.width) / 2.0), y: floor((panelHeight + combinedHeight) / 2.0) - subtitleSize.height), size: subtitleSize) + self.subtitleNode.frame = subtitleFrame + + let combinedFrame = textFrame.union(subtitleFrame) + self.buttonNode.frame = combinedFrame.insetBy(dx: -8.0, dy: -12.0) return panelHeight } diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 915121923c..aa7b9960a4 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -296,6 +296,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, ASScrollViewDe }, openWebApp: { _ in }, openPhotoSetup: { }, openAdInfo: { _ in + }, openAccountFreezeInfo: { }) interaction.searchTextHighightState = searchQuery self.interaction = interaction diff --git a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index 855cc81ef2..7fdf358ddb 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -183,6 +183,8 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable { openPhotoSetup: { }, openAdInfo: { _ in + }, + openAccountFreezeInfo: { } ) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 753e8f3c60..9968f1e98b 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -77,6 +77,7 @@ import ContentReportScreen import AffiliateProgramSetupScreen import GalleryUI import ShareController +import AccountFreezeInfoScreen private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -3469,6 +3470,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { } return controller } + + public func makeAccountFreezeInfoScreen(context: AccountContext) -> ViewController { + return AccountFreezeInfoScreen(context: context) + } } private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? { From ebfa8f08a1d900fa289be5d79382cd1358be1631 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 10 Mar 2025 18:17:55 +0400 Subject: [PATCH 12/19] Fix gift message flag --- .../Sources/TelegramEngine/Payments/BotPaymentForm.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index b5c2e11a66..8817f48613 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -395,7 +395,7 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv var flags: Int32 = 0 var message: Api.TextWithEntities? if let text, !text.isEmpty { - flags |= (1 << 1) + flags |= (1 << 0) message = .textWithEntities(text: text, entities: entities.flatMap { apiEntitiesFromMessageTextEntities($0, associatedPeers: SimpleDictionary()) } ?? []) } return .inputInvoicePremiumGiftStars(flags: flags, userId: inputUser, months: option.months, message: message) From 80cd8f7b320652152349e7ce3494f4bbd96550e6 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 11 Mar 2025 06:49:43 +0400 Subject: [PATCH 13/19] Various improvements --- .../Sources/AccountContext.swift | 2 + submodules/AuthorizationUI/BUILD | 1 + .../AuthorizationSequenceController.swift | 5 +- .../AuthorizationSequencePaymentScreen.swift | 339 +++++++- .../Sources/InAppPurchaseManager.swift | 2 + submodules/PremiumUI/BUILD | 1 + .../Sources/BusinessPageComponent.swift | 1 + .../Sources/PremiumIntroScreen.swift | 746 +++++++++++------- .../Sources/PremiumLimitsListScreen.swift | 1 + submodules/TelegramApi/Sources/Api0.swift | 2 +- submodules/TelegramApi/Sources/Api29.swift | 18 +- .../Sources/Account/Account.swift | 95 ++- .../TelegramCore/Sources/Authorization.swift | 25 +- .../UnauthorizedAccountStateManager.swift | 18 +- .../SyncCore_UnauthorizedAccountState.swift | 12 +- .../TelegramEngine/Payments/AppStore.swift | 23 +- .../Payments/TelegramEnginePayments.swift | 2 +- .../ChatEmptyNode/Sources/ChatEmptyNode.swift | 23 +- .../ChatMessageAnimatedStickerItemNode.swift | 4 + .../ChatMessageAttachedContentNode.swift | 4 + .../Sources/ChatMessageBubbleItemNode.swift | 6 +- .../ChatMessageContactBubbleContentNode.swift | 4 + .../ChatMessageDateAndStatusNode.swift | 106 +++ ...hatMessageFactCheckBubbleContentNode.swift | 4 + ...ChatMessageGiveawayBubbleContentNode.swift | 1 + .../ChatMessageInteractiveFileNode.swift | 4 + ...atMessageInteractiveInstantVideoNode.swift | 4 + .../ChatMessageInteractiveMediaNode.swift | 4 + .../ChatMessageMapBubbleContentNode.swift | 4 + .../ChatMessageMediaBubbleContentNode.swift | 4 + .../ChatMessagePollBubbleContentNode.swift | 4 + ...atMessageRestrictedBubbleContentNode.swift | 4 + .../Sources/ChatMessageStickerItemNode.swift | 4 + .../ChatMessageTextBubbleContentNode.swift | 4 + .../AffiliateProgramSetupScreen/BUILD | 2 +- .../Sources/AffiliateProgramSetupScreen.swift | 2 +- .../Premium/PremiumCoinComponent/BUILD | 27 + .../Sources/PremiumCoinComponent.swift | 5 +- .../Premium/Authorization/Contents.json | 9 + .../Authorization/Cost.imageset/Contents.json | 12 + .../Cost.imageset/highprice_30.pdf | Bin 0 -> 4940 bytes .../Support.imageset/Contents.json | 12 + .../Support.imageset/support_30.pdf | Bin 0 -> 14530 bytes .../Verification.imageset/Contents.json | 12 + .../verificationcode_30.pdf | Bin 0 -> 4908 bytes .../Sources/SharedAccountContext.swift | 24 +- 46 files changed, 1200 insertions(+), 386 deletions(-) create mode 100644 submodules/TelegramUI/Components/Premium/PremiumCoinComponent/BUILD rename submodules/{PremiumUI => TelegramUI/Components/Premium/PremiumCoinComponent}/Sources/PremiumCoinComponent.swift (99%) create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Authorization/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Authorization/Cost.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Authorization/Cost.imageset/highprice_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Authorization/Support.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Authorization/Support.imageset/support_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Authorization/Verification.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Authorization/Verification.imageset/verificationcode_30.pdf diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index eacb803483..aaa6ae47f3 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1053,6 +1053,8 @@ public protocol SharedAccountContext: AnyObject { func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?, temporary: Bool) -> ViewController func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController + func makePremiumIntroController(sharedContext: SharedAccountContext, engine: TelegramEngineUnauthorized, inAppPurchaseManager: InAppPurchaseManager, source: PremiumIntroSource, dismissed: (() -> Void)?) -> ViewController + func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, forceDark: Bool, action: @escaping () -> Void, dismissed: (() -> Void)?) -> ViewController func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController diff --git a/submodules/AuthorizationUI/BUILD b/submodules/AuthorizationUI/BUILD index db3854ecc2..a0657ad3e9 100644 --- a/submodules/AuthorizationUI/BUILD +++ b/submodules/AuthorizationUI/BUILD @@ -43,6 +43,7 @@ swift_library( "//submodules/MoreButtonNode:MoreButtonNode", "//submodules/ContextUI:ContextUI", "//submodules/InAppPurchaseManager", + "//submodules/TelegramUI/Components/Premium/PremiumCoinComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index 47c1c5b875..0ddd8a45c6 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -764,12 +764,11 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth } private func paymentController(number: String, phoneCodeHash: String, storeProduct: String) -> AuthorizationSequencePaymentScreen { - let controller = AuthorizationSequencePaymentScreen(engine: self.engine, presentationData: self.presentationData, inAppPurchaseManager: self.inAppPurchaseManager, phoneNumber: number, phoneCodeHash: phoneCodeHash, storeProduct: storeProduct, back: { [weak self] in + let controller = AuthorizationSequencePaymentScreen(sharedContext: self.sharedContext, engine: self.engine, presentationData: self.presentationData, inAppPurchaseManager: self.inAppPurchaseManager, phoneNumber: number, phoneCodeHash: phoneCodeHash, storeProduct: storeProduct, back: { [weak self] in guard let self else { return } let countryCode = AuthorizationSequenceController.defaultCountryCode() - let _ = self.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: self.account.testingEnvironment, masterDatacenterId: self.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() }) return controller @@ -1302,7 +1301,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth } controllers.append(self.signUpController(firstName: firstName, lastName: lastName, termsOfService: termsOfService, displayCancel: displayCancel)) self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty) - case let .payment(number, codeHash, storeProduct): + case let .payment(number, codeHash, storeProduct, _): var controllers: [ViewController] = [] if !self.otherAccountPhoneNumbers.1.isEmpty { controllers.append(self.splashController()) diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift index 55efdae6f7..914bfb8934 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift @@ -14,15 +14,19 @@ import ViewControllerComponent import MultilineTextComponent import BalancedTextComponent import BundleIconComponent -import LottieComponent import ButtonComponent import TextFormat import InAppPurchaseManager import ConfettiEffect +import PremiumCoinComponent +import Markdown +import CountrySelectionUI +import AccountContext final class AuthorizationSequencePaymentScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment + let sharedContext: SharedAccountContext let engine: TelegramEngineUnauthorized let inAppPurchaseManager: InAppPurchaseManager let presentationData: PresentationData @@ -31,6 +35,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component { let storeProduct: String init( + sharedContext: SharedAccountContext, engine: TelegramEngineUnauthorized, inAppPurchaseManager: InAppPurchaseManager, presentationData: PresentationData, @@ -38,6 +43,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component { phoneCodeHash: String, storeProduct: String ) { + self.sharedContext = sharedContext self.engine = engine self.inAppPurchaseManager = inAppPurchaseManager self.presentationData = presentationData @@ -93,9 +99,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component { self.state?.updated() let (currency, amount) = storeProduct.priceCurrencyAndAmount - let purpose: AppStoreTransactionPurpose = .authCode(restore: false, phoneNumber: component.phoneNumber, phoneCodeHash: component.phoneCodeHash, currency: currency, amount: amount) - let _ = (component.engine.payments.canPurchasePremium(purpose: purpose) |> deliverOnMainQueue).start(next: { [weak self] available in guard let self else { @@ -111,6 +115,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component { guard let self, let controller = self.environment?.controller() else { return } + self.inProgress = false self.state?.updated(transition: .immediate) var errorText: String? @@ -156,7 +161,6 @@ final class AuthorizationSequencePaymentScreenComponent: Component { let environment = environment[EnvironmentType.self].value let themeUpdated = self.environment?.theme !== environment.theme self.environment = environment - self.component = component self.state = state if self.component == nil { @@ -166,38 +170,150 @@ final class AuthorizationSequencePaymentScreenComponent: Component { return } self.products = products - self.state?.updated() + if !self.isUpdating { + self.state?.updated() + } }) } + self.component = component + if themeUpdated { self.backgroundColor = environment.theme.list.plainBackgroundColor } + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left - let animationHeight: CGFloat = 120.0 let animationSize = self.animation.update( transition: transition, - component: AnyComponent(LottieComponent( - content: LottieComponent.AppBundleContent(name: "Coin"), - startingPosition: .begin + component: AnyComponent(PremiumCoinComponent( + mode: .business, + isIntro: true, + isVisible: true, + hasIdleAnimations: true )), environment: {}, - containerSize: CGSize(width: animationHeight, height: animationHeight) + containerSize: CGSize(width: min(414.0, availableSize.width), height: 184.0) ) + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "SMS Fee", font: Font.bold(28.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor))) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + + let textColor = environment.theme.list.itemPrimaryTextColor + let secondaryTextColor = environment.theme.list.itemSecondaryTextColor + let linkColor = environment.theme.list.itemAccentColor + + var countryName: String = "" + if let (country, _) = AuthorizationSequenceCountrySelectionController.lookupCountryIdByNumber(component.phoneNumber, preferredCountries: [:]) { + countryName = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(country.id, strings: environment.strings) ?? country.name + } + + var items: [AnyComponentWithIdentity] = [] + items.append( + AnyComponentWithIdentity( + id: "cost", + component: AnyComponent(ParagraphComponent( + title: "High SMS Costs", + titleColor: textColor, + text: "Telecom providers in your country (\(countryName)) charge Telegram very high prices for SMS.", + textColor: secondaryTextColor, + iconName: "Premium/Authorization/Cost", + iconColor: linkColor + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "verification", + component: AnyComponent(ParagraphComponent( + title: "Verification Required", + titleColor: textColor, + text: "Telegram needs to send you an SMS with a verification code to confirm your phone number.", + textColor: secondaryTextColor, + iconName: "Premium/Authorization/Verification", + iconColor: linkColor + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "withdrawal", + component: AnyComponent(ParagraphComponent( + title: "Support via [Telegram Premium >]()", + titleColor: textColor, + text: "Sign up for a 1-week Telegram Premium subscription to help cover the SMS costs.", + textColor: secondaryTextColor, + iconName: "Premium/Authorization/Support", + iconColor: linkColor, + action: { [weak self] in + guard let self, let controller = self.environment?.controller() else { + return + } + let introController = component.sharedContext.makePremiumIntroController( + sharedContext: component.sharedContext, + engine: component.engine, + inAppPurchaseManager: component.inAppPurchaseManager, + source: .about, + dismissed: nil + ) + controller.push(introController) + } + )) + ) + ) + + let listSize = self.list.update( + transition: transition, + component: AnyComponent(List(items)), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + + let titleSpacing: CGFloat = -24.0 + let listSpacing: CGFloat = 12.0 + let totalHeight = animationSize.height + titleSpacing + titleSize.height + listSpacing + listSize.height + + var originY = floor((availableSize.height - totalHeight) / 2.0) + if let animationView = self.animation.view { if animationView.superview == nil { self.addSubview(animationView) } - animationView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - animationSize.width) / 2.0), y: 156.0), size: animationSize) + animationView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - animationSize.width) / 2.0), y: originY), size: animationSize) + originY += animationSize.height + titleSpacing + } + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: originY), size: titleSize) + originY += titleSize.height + listSpacing + } + + if let listView = self.list.view { + if listView.superview == nil { + self.addSubview(listView) + } + listView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - listSize.width) / 2.0), y: originY), size: listSize) } let buttonHeight: CGFloat = 50.0 let bottomPanelPadding: CGFloat = 12.0 let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset - - let sideInset: CGFloat = 16.0 - let buttonString = "Sign up for $1" + + let priceString: String + if let product = self.products.first(where: { $0.id == component.storeProduct }) { + priceString = product.price + } else { + priceString = "–" + } + let buttonString = "Sign up for \(priceString)" let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) let buttonSize = self.button.update( transition: transition, @@ -210,7 +326,12 @@ final class AuthorizationSequencePaymentScreenComponent: Component { ), content: AnyComponentWithIdentity( id: AnyHashable(buttonString), - component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) + component: AnyComponent( + VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Get Telegram Premium for 1 week", font: Font.regular(11.0), textColor: environment.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), paragraphAlignment: .center))))) + ], spacing: 1.0) + ) ), isEnabled: true, displaysProgress: self.inProgress, @@ -243,6 +364,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component { public final class AuthorizationSequencePaymentScreen: ViewControllerComponentContainer { public init( + sharedContext: SharedAccountContext, engine: TelegramEngineUnauthorized, presentationData: PresentationData, inAppPurchaseManager: InAppPurchaseManager, @@ -252,6 +374,7 @@ public final class AuthorizationSequencePaymentScreen: ViewControllerComponentCo back: @escaping () -> Void ) { super.init(component: AuthorizationSequencePaymentScreenComponent( + sharedContext: sharedContext, engine: engine, inAppPurchaseManager: inAppPurchaseManager, presentationData: presentationData, @@ -260,6 +383,12 @@ public final class AuthorizationSequencePaymentScreen: ViewControllerComponentCo storeProduct: storeProduct ), navigationBarAppearance: .transparent, theme: .default, updatedPresentationData: (initial: presentationData, signal: .single(presentationData))) + loadServerCountryCodes(accountManager: sharedContext.accountManager, engine: engine, completion: { [weak self] in + if let strongSelf = self { + strongSelf.requestLayout(forceUpdate: true, transition: .immediate) + } + }) + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) @@ -285,3 +414,183 @@ public final class AuthorizationSequencePaymentScreen: ViewControllerComponentCo self.dismiss() } } + +private final class ParagraphComponent: CombinedComponent { + let title: String + let titleColor: UIColor + let text: String + let textColor: UIColor + let iconName: String + let iconColor: UIColor + let action: (() -> Void)? + + public init( + title: String, + titleColor: UIColor, + text: String, + textColor: UIColor, + iconName: String, + iconColor: UIColor, + action: (() -> Void)? = nil + ) { + self.title = title + self.titleColor = titleColor + self.text = text + self.textColor = textColor + self.iconName = iconName + self.iconColor = iconColor + self.action = action + } + + static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.titleColor != rhs.titleColor { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.textColor != rhs.textColor { + return false + } + if lhs.iconName != rhs.iconName { + return false + } + if lhs.iconColor != rhs.iconColor { + return false + } + return true + } + + final class State: ComponentState { + var cachedChevronImage: (UIImage, UIColor)? + } + + func makeState() -> State { + return State() + } + + static var body: Body { + let title = Child(MultilineTextComponent.self) + let text = Child(MultilineTextComponent.self) + let icon = Child(BundleIconComponent.self) + + return { context in + let component = context.component + let state = context.state + + let leftInset: CGFloat = 64.0 + let rightInset: CGFloat = 32.0 + let textSideInset: CGFloat = leftInset + 8.0 + let spacing: CGFloat = 5.0 + + let textTopInset: CGFloat = 9.0 + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let titleColor = component.titleColor + let textColor = component.textColor + let linkColor = component.iconColor + let titleMarkdownAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: boldTextFont, textColor: titleColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: titleColor), + link: MarkdownAttributeSet(font: boldTextFont, textColor: linkColor), + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + } + ) + + if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== linkColor { + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, linkColor) + } + + let titleAttributedString = parseMarkdownIntoAttributedString(component.title, attributes: titleMarkdownAttributes).mutableCopy() as! NSMutableAttributedString + if let range = titleAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { + titleAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: titleAttributedString.string)) + } + + let title = title.update( + component: MultilineTextComponent( + text: .plain(titleAttributedString), + horizontalAlignment: .center, + maximumNumberOfLines: 1, + highlightColor: linkColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { attributes, _ in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + component.action?() + } + } + ), + availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + + let textMarkdownAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: linkColor), + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + } + ) + + let text = text.update( + component: MultilineTextComponent( + text: .markdown(text: component.text, attributes: textMarkdownAttributes), + horizontalAlignment: .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.2, + highlightColor: linkColor.withAlphaComponent(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { attributes, _ in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + component.action?() + } + } + ), + availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height), + transition: .immediate + ) + + let icon = icon.update( + component: BundleIconComponent( + name: component.iconName, + tintColor: component.iconColor + ), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), + transition: .immediate + ) + + context.add(title + .position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) + ) + + context.add(text + .position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0)) + ) + + context.add(icon + .position(CGPoint(x: 47.0, y: textTopInset + 18.0)) + ) + + return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 18.0) + } + } +} diff --git a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift index 2bb095f7f4..8dc211102b 100644 --- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift +++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift @@ -28,6 +28,8 @@ private let productIdentifiers = [ "org.telegram.telegramPremium.sixMonths.code_x10", "org.telegram.telegramPremium.twelveMonths.code_x10", + "org.telegram.telegramPremium.oneWeek.auth", + "org.telegram.telegramStars.topup.x15", "org.telegram.telegramStars.topup.x25", "org.telegram.telegramStars.topup.x50", diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index 9cad24de43..6254850f9e 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -120,6 +120,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiActionIconComponent", "//submodules/TelegramUI/Components/ScrollComponent", "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", + "//submodules/TelegramUI/Components/Premium/PremiumCoinComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/PremiumUI/Sources/BusinessPageComponent.swift b/submodules/PremiumUI/Sources/BusinessPageComponent.swift index 72ae9e5639..46c469d7ae 100644 --- a/submodules/PremiumUI/Sources/BusinessPageComponent.swift +++ b/submodules/PremiumUI/Sources/BusinessPageComponent.swift @@ -11,6 +11,7 @@ import Markdown import TelegramPresentationData import BundleIconComponent import ScrollComponent +import PremiumCoinComponent private final class HeaderComponent: Component { let context: AccountContext diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 7d0d43370a..61175ca87b 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -34,6 +34,7 @@ import EntityKeyboard import EmojiActionIconComponent import ScrollComponent import PremiumStarComponent +import PremiumCoinComponent public enum PremiumSource: Equatable { public static func == (lhs: PremiumSource, rhs: PremiumSource) -> Bool { @@ -1403,7 +1404,7 @@ final class PerkComponent: CombinedComponent { private final class PremiumIntroScreenContentComponent: CombinedComponent { typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment) - let context: AccountContext + let screenContext: PremiumIntroScreen.ScreenContext let mode: PremiumIntroScreen.Mode let source: PremiumSource let forceDark: Bool @@ -1423,7 +1424,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let shareLink: (String) -> Void init( - context: AccountContext, + screenContext: PremiumIntroScreen.ScreenContext, mode: PremiumIntroScreen.Mode, source: PremiumSource, forceDark: Bool, @@ -1442,7 +1443,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { copyLink: @escaping (String) -> Void, shareLink: @escaping (String) -> Void ) { - self.context = context + self.screenContext = screenContext self.mode = mode self.source = source self.forceDark = forceDark @@ -1463,9 +1464,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } static func ==(lhs: PremiumIntroScreenContentComponent, rhs: PremiumIntroScreenContentComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } if lhs.source != rhs.source { return false } @@ -1498,7 +1496,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } final class State: ComponentState { - private let context: AccountContext + private let screenContext: PremiumIntroScreen.ScreenContext private let present: (ViewController) -> Void var products: [PremiumProduct]? @@ -1542,104 +1540,122 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { var cachedChevronImage: (UIImage, PresentationTheme)? init( - context: AccountContext, + screenContext: PremiumIntroScreen.ScreenContext, source: PremiumSource, present: @escaping (ViewController) -> Void ) { - self.context = context + self.screenContext = screenContext self.present = present super.init() - self.disposable = (context.engine.data.subscribe( - TelegramEngine.EngineData.Item.Configuration.App(), - TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId) - ) - |> deliverOnMainQueue).start(next: { [weak self] appConfiguration, accountPeer in - if let strongSelf = self { - let isFirstTime = strongSelf.peer == nil + let premiumIntroConfiguration: Signal + let accountPeer: Signal + switch screenContext { + case let .accountContext(context): + premiumIntroConfiguration = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App()) + |> map { appConfiguration in + return PremiumIntroConfiguration.with(appConfiguration: appConfiguration) + } + accountPeer = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + case .sharedContext: + premiumIntroConfiguration = .single(PremiumIntroConfiguration.defaultValue) + accountPeer = .single(nil) + } + + self.disposable = combineLatest( + queue: Queue.mainQueue(), + premiumIntroConfiguration, + accountPeer + ).start(next: { [weak self] premiumIntroConfiguration, accountPeer in + guard let self else { + return + } + let isFirstTime = self.peer == nil + + self.configuration = premiumIntroConfiguration + self.peer = accountPeer + self.updated(transition: .immediate) + + if let identifier = source.identifier, isFirstTime { + var jsonString: String = "{" + jsonString += "\"source\": \"\(identifier)\"," - strongSelf.configuration = PremiumIntroConfiguration.with(appConfiguration: appConfiguration) - strongSelf.peer = accountPeer - strongSelf.updated(transition: .immediate) + jsonString += "\"data\": {\"premium_promo_order\":[" + var isFirst = true + for perk in premiumIntroConfiguration.perks { + if !isFirst { + jsonString += "," + } + isFirst = false + jsonString += "\"\(perk.identifier)\"" + } + jsonString += "]}}" - if let identifier = source.identifier, isFirstTime { - var jsonString: String = "{" - jsonString += "\"source\": \"\(identifier)\"," - - jsonString += "\"data\": {\"premium_promo_order\":[" - var isFirst = true - for perk in strongSelf.configuration.perks { - if !isFirst { - jsonString += "," - } - isFirst = false - jsonString += "\"\(perk.identifier)\"" - } - jsonString += "]}}" - - if let data = jsonString.data(using: .utf8), let json = JSON(data: data) { - addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_show", data: json) - } + if let context = screenContext.context, let data = jsonString.data(using: .utf8), let json = JSON(data: data) { + addAppLogEvent(postbox: context.account.postbox, type: "premium.promo_screen_show", data: json) } } }) - let _ = updatePremiumPromoConfigurationOnce(account: context.account).start() - - let stickersKey: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.CloudPremiumStickers) - self.stickersDisposable = (self.context.account.postbox.combinedView(keys: [stickersKey]) - |> deliverOnMainQueue).start(next: { [weak self] views in - guard let strongSelf = self else { - return - } - if let view = views.views[stickersKey] as? OrderedItemListView { - for item in view.items { - if let mediaItem = item.contents.get(RecentMediaItem.self) { - let file = mediaItem.media._parse() - strongSelf.preloadDisposableSet.add(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start()) - if let effect = file.videoThumbnails.first { - strongSelf.preloadDisposableSet.add(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: effect.resource).start()) + if let context = screenContext.context { + let _ = updatePremiumPromoConfigurationOnce(account: context.account).start() + + let stickersKey: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.CloudPremiumStickers) + self.stickersDisposable = (context.account.postbox.combinedView(keys: [stickersKey]) + |> deliverOnMainQueue).start(next: { [weak self] views in + guard let self else { + return + } + if let view = views.views[stickersKey] as? OrderedItemListView { + for item in view.items { + if let mediaItem = item.contents.get(RecentMediaItem.self) { + let file = mediaItem.media._parse() + self.preloadDisposableSet.add(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start()) + if let effect = file.videoThumbnails.first { + self.preloadDisposableSet.add(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: effect.resource).start()) + } } } } - } - }) - - self.newPerksDisposable = combineLatest(queue: Queue.mainQueue(), - ApplicationSpecificNotice.dismissedBusinessBadge(accountManager: context.sharedContext.accountManager), - ApplicationSpecificNotice.dismissedBusinessLinksBadge(accountManager: context.sharedContext.accountManager), - ApplicationSpecificNotice.dismissedBusinessIntroBadge(accountManager: context.sharedContext.accountManager), - ApplicationSpecificNotice.dismissedBusinessChatbotsBadge(accountManager: context.sharedContext.accountManager) - ).startStrict(next: { [weak self] dismissedBusinessBadge, dismissedBusinessLinksBadge, dismissedBusinessIntroBadge, dismissedBusinessChatbotsBadge in - guard let self else { - return - } - var newPerks: [String] = [] - if !dismissedBusinessBadge { - newPerks.append(PremiumPerk.business.identifier) - } - if !dismissedBusinessLinksBadge { - newPerks.append(PremiumPerk.businessLinks.identifier) - } - if !dismissedBusinessIntroBadge { - newPerks.append(PremiumPerk.businessIntro.identifier) - } - if !dismissedBusinessChatbotsBadge { - newPerks.append(PremiumPerk.businessChatBots.identifier) - } - self.newPerks = newPerks - self.updated() - }) - - self.adsEnabledDisposable = (context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.AdsEnabled(id: context.account.peerId)) - |> deliverOnMainQueue).start(next: { [weak self] adsEnabled in - guard let self else { - return - } - self.adsEnabled = adsEnabled - self.updated() - }) + }) + + self.newPerksDisposable = combineLatest( + queue: Queue.mainQueue(), + ApplicationSpecificNotice.dismissedBusinessBadge(accountManager: context.sharedContext.accountManager), + ApplicationSpecificNotice.dismissedBusinessLinksBadge(accountManager: context.sharedContext.accountManager), + ApplicationSpecificNotice.dismissedBusinessIntroBadge(accountManager: context.sharedContext.accountManager), + ApplicationSpecificNotice.dismissedBusinessChatbotsBadge(accountManager: context.sharedContext.accountManager) + ).startStrict(next: { [weak self] dismissedBusinessBadge, dismissedBusinessLinksBadge, dismissedBusinessIntroBadge, dismissedBusinessChatbotsBadge in + guard let self else { + return + } + var newPerks: [String] = [] + if !dismissedBusinessBadge { + newPerks.append(PremiumPerk.business.identifier) + } + if !dismissedBusinessLinksBadge { + newPerks.append(PremiumPerk.businessLinks.identifier) + } + if !dismissedBusinessIntroBadge { + newPerks.append(PremiumPerk.businessIntro.identifier) + } + if !dismissedBusinessChatbotsBadge { + newPerks.append(PremiumPerk.businessChatBots.identifier) + } + self.newPerks = newPerks + self.updated() + }) + + self.adsEnabledDisposable = (context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.AdsEnabled(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] adsEnabled in + guard let self else { + return + } + self.adsEnabled = adsEnabled + self.updated() + }) + } } deinit { @@ -1655,6 +1671,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { private weak var emojiStatusSelectionController: ViewController? private var previousEmojiSetupTimestamp: Double? func openEmojiSetup(sourceView: UIView, currentFileId: Int64?, color: UIColor?) { + guard let context = self.screenContext.context else { + return + } let currentTimestamp = CACurrentMediaTime() if let previousTimestamp = self.previousEmojiSetupTimestamp, currentTimestamp < previousTimestamp + 1.0 { return @@ -1668,20 +1687,20 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } let controller = EmojiStatusSelectionController( - context: self.context, + context: context, mode: .statusSelection, sourceView: sourceView, emojiContent: EmojiPagerContentComponent.emojiInputData( - context: self.context, - animationCache: self.context.animationCache, - animationRenderer: self.context.animationRenderer, + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, isStandalone: false, subject: .status, hasTrending: false, topReactionItems: [], areUnicodeEmojiEnabled: false, areCustomEmojiEnabled: true, - chatPeerId: self.context.account.peerId, + chatPeerId: context.account.peerId, selectedItems: selectedItems, topStatusTitle: nil, backgroundIconColor: color @@ -1701,7 +1720,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, source: self.source, present: self.present) + return State(screenContext: self.screenContext, source: self.source, present: self.present) } static var body: Body { @@ -1733,7 +1752,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let theme = environment.theme let strings = environment.strings - let presentationData = context.component.context.sharedContext.currentPresentationData.with { $0 } + let presentationData = context.component.screenContext.presentationData let availableWidth = context.availableSize.width let sideInsets = sideInset * 2.0 + environment.safeInsets.left + environment.safeInsets.right @@ -1786,8 +1805,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } else if case .giftTerms = context.component.source { textString = strings.Premium_PersonalDescription } else if let _ = context.component.otherPeerName { - if case let .gift(fromId, _, _, giftCode) = context.component.source { - if fromId == context.component.context.account.peerId { + if case let .gift(fromId, _, _, giftCode) = context.component.source, let accountContext = context.component.screenContext.context { + if fromId == accountContext.account.peerId { textString = strings.Premium_GiftedDescriptionYou } else { if let giftCode { @@ -1895,7 +1914,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { UIColor(rgb: 0x3dbd4a) ] - let accountContext = context.component.context + let accountContext = context.component.screenContext.context let present = context.component.present let push = context.component.push let selectProduct = context.component.selectProduct @@ -2068,6 +2087,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { iconName: perk.iconName ))), false), action: { [weak state] _ in + guard let accountContext else { + return + } var demoSubject: PremiumDemoScreen.Subject switch perk { case .doubleLimits: @@ -2239,6 +2261,10 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { iconName: perk.iconName ))), false), action: { [weak state] _ in + guard let accountContext else { + return + } + let isPremium = state?.isPremium == true if isPremium { switch perk { @@ -2396,46 +2422,48 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let accentColor = environment.theme.list.itemAccentColor var perksItems: [AnyComponentWithIdentity] = [] - perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - title: AnyComponent(VStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: strings.Business_SetEmojiStatus, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 0 - ))), - AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: strings.Business_SetEmojiStatusInfo, - font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)), - textColor: environment.theme.list.itemSecondaryTextColor - )), - maximumNumberOfLines: 0, - lineSpacing: 0.18 - ))) - ], alignment: .left, spacing: 2.0)), - leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent( - backgroundColor: UIColor(rgb: 0x676bff), - foregroundColor: .white, - iconName: "Premium/BusinessPerk/Status" - ))), false), - icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( - context: context.component.context, - color: accentColor, - fileId: status?.fileId, - file: nil - )))), - accessory: nil, - action: { [weak state] view in - guard let view = view as? ListActionItemComponent.View, let iconView = view.iconView else { - return + if let accountContext = context.component.screenContext.context { + perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Business_SetEmojiStatus, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 0 + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Business_SetEmojiStatusInfo, + font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)), + textColor: environment.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 0, + lineSpacing: 0.18 + ))) + ], alignment: .left, spacing: 2.0)), + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent( + backgroundColor: UIColor(rgb: 0x676bff), + foregroundColor: .white, + iconName: "Premium/BusinessPerk/Status" + ))), false), + icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( + context: accountContext, + color: accentColor, + fileId: status?.fileId, + file: nil + )))), + accessory: nil, + action: { [weak state] view in + guard let view = view as? ListActionItemComponent.View, let iconView = view.iconView else { + return + } + state?.openEmojiSetup(sourceView: iconView, currentFileId: nil, color: accentColor) } - state?.openEmojiSetup(sourceView: iconView, currentFileId: nil, color: accentColor) - } - )))) + )))) + } perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent( theme: environment.theme, @@ -2464,6 +2492,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { iconName: "Premium/BusinessPerk/Tag" ))), false), action: { _ in + guard let accountContext else { + return + } push(accountContext.sharedContext.makeFilterSettingsController(context: accountContext, modal: false, scrollToTags: true, dismissed: nil)) } )))) @@ -2495,6 +2526,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { iconName: "Premium/Perk/Stories" ))), false), action: { _ in + guard let accountContext else { + return + } push(accountContext.sharedContext.makeMyStoriesController(context: accountContext, isArchive: false)) } )))) @@ -2560,6 +2594,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { ))), ], alignment: .left, spacing: 2.0)), accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: state.adsEnabled, action: { [weak state] value in + guard let accountContext else { + return + } let _ = accountContext.engine.accountData.updateAdMessagesEnabled(enabled: value).startStandalone() state?.updated(transition: .immediate) })), @@ -2576,8 +2613,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } let controller = environment.controller let adsInfoTapActionImpl: ([NSAttributedString.Key: Any]) -> Void = { _ in - if let controller = controller() as? PremiumIntroScreen { - controller.context.sharedContext.openExternalUrl(context: controller.context, urlContext: .generic, url: environment.strings.Business_AdsInfo_URL, forceExternal: true, presentationData: controller.context.sharedContext.currentPresentationData.with({$0}), navigationController: nil, dismissInput: {}) + if let controller = controller() as? PremiumIntroScreen, let context = controller.context { + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: environment.strings.Business_AdsInfo_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) } } let adsSettingsSection = adsSettingsSection.update( @@ -2627,7 +2664,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { layoutPerks() layoutOptions() } else if case let .gift(fromPeerId, _, _, giftCode) = context.component.source { - if let giftCode, fromPeerId != context.component.context.account.peerId, !context.component.justBought { + if let giftCode, let accountContext = context.component.screenContext.context, fromPeerId != accountContext.account.peerId, !context.component.justBought { let link = "https://t.me/giftcode/\(giftCode.slug)" let linkButton = linkButton.update( component: Button( @@ -2718,7 +2755,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { var isGiftView = false if case let .gift(fromId, _, _, _) = context.component.source { - if fromId == context.component.context.account.peerId { + if let accountContext = context.component.screenContext.context, fromId == accountContext.account.peerId { isGiftView = true } } @@ -2738,14 +2775,12 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let controller = environment.controller let termsTapActionImpl: ([NSAttributedString.Key: Any]) -> Void = { attributes in - if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String, - let controller = controller() as? PremiumIntroScreen, let navigationController = controller.navigationController as? NavigationController { + if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String, let controller = controller() as? PremiumIntroScreen, let context = controller.context, let navigationController = controller.navigationController as? NavigationController { if url.hasPrefix("https://apps.apple.com/account/subscriptions") { - controller.context.sharedContext.applicationBindings.openSubscriptions() + context.sharedContext.applicationBindings.openSubscriptions() } else if url.hasPrefix("https://") || url.hasPrefix("tg://") { - controller.context.sharedContext.openExternalUrl(context: controller.context, urlContext: .generic, url: url, forceExternal: false, presentationData: controller.context.sharedContext.currentPresentationData.with({$0}), navigationController: navigationController, dismissInput: {}) + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) } else { - let context = controller.context let signal: Signal? switch url { case "terms": @@ -2815,7 +2850,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { private final class PremiumIntroScreenComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment - let context: AccountContext + let screenContext: PremiumIntroScreen.ScreenContext let mode: PremiumIntroScreen.Mode let source: PremiumSource let forceDark: Bool @@ -2827,8 +2862,8 @@ private final class PremiumIntroScreenComponent: CombinedComponent { let copyLink: (String) -> Void let shareLink: (String) -> Void - init(context: AccountContext, mode: PremiumIntroScreen.Mode, source: PremiumSource, forceDark: Bool, forceHasPremium: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, completion: @escaping () -> Void, copyLink: @escaping (String) -> Void, shareLink: @escaping (String) -> Void) { - self.context = context + init(screenContext: PremiumIntroScreen.ScreenContext, mode: PremiumIntroScreen.Mode, source: PremiumSource, forceDark: Bool, forceHasPremium: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, completion: @escaping () -> Void, copyLink: @escaping (String) -> Void, shareLink: @escaping (String) -> Void) { + self.screenContext = screenContext self.mode = mode self.source = source self.forceDark = forceDark @@ -2842,9 +2877,6 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } static func ==(lhs: PremiumIntroScreenComponent, rhs: PremiumIntroScreenComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } if lhs.mode != rhs.mode { return false } @@ -2861,7 +2893,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } final class State: ComponentState { - private let context: AccountContext + private let screenContext: PremiumIntroScreen.ScreenContext private let source: PremiumSource private let updateInProgress: (Bool) -> Void private let present: (ViewController) -> Void @@ -2883,10 +2915,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { var isPremium: Bool? var otherPeerName: String? var justBought = false - - let animationCache: AnimationCache - let animationRenderer: MultiAnimationRenderer - + var emojiFile: TelegramMediaFile? var emojiPackTitle: String? private var emojiFileDisposable: Disposable? @@ -2917,43 +2946,44 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } } - init(context: AccountContext, source: PremiumSource, forceHasPremium: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void) { - self.context = context + init(screenContext: PremiumIntroScreen.ScreenContext, source: PremiumSource, forceHasPremium: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void) { + self.screenContext = screenContext self.source = source self.updateInProgress = updateInProgress self.present = present self.completion = completion - - self.animationCache = context.animationCache - self.animationRenderer = context.animationRenderer - + super.init() - self.validPurchases = context.inAppPurchaseManager?.getReceiptPurchases() ?? [] + self.validPurchases = screenContext.inAppPurchaseManager?.getReceiptPurchases() ?? [] let availableProducts: Signal<[InAppPurchaseManager.Product], NoError> - if let inAppPurchaseManager = context.inAppPurchaseManager { + if let inAppPurchaseManager = screenContext.inAppPurchaseManager { availableProducts = inAppPurchaseManager.availableProducts } else { availableProducts = .single([]) } let otherPeerName: Signal - if case let .gift(fromPeerId, toPeerId, _, _) = source { - let otherPeerId = fromPeerId != context.account.peerId ? fromPeerId : toPeerId - otherPeerName = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: otherPeerId)) - |> map { peer -> String? in - return peer?.compactDisplayTitle - } - } else if case let .profile(peerId) = source { - otherPeerName = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> map { peer -> String? in - return peer?.compactDisplayTitle - } - } else if case let .emojiStatus(peerId, _, _, _) = source { - otherPeerName = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> map { peer -> String? in - return peer?.compactDisplayTitle + if let context = screenContext.context { + if case let .gift(fromPeerId, toPeerId, _, _) = source { + let otherPeerId = fromPeerId != context.account.peerId ? fromPeerId : toPeerId + otherPeerName = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: otherPeerId)) + |> map { peer -> String? in + return peer?.compactDisplayTitle + } + } else if case let .profile(peerId) = source { + otherPeerName = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> map { peer -> String? in + return peer?.compactDisplayTitle + } + } else if case let .emojiStatus(peerId, _, _, _) = source { + otherPeerName = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> map { peer -> String? in + return peer?.compactDisplayTitle + } + } else { + otherPeerName = .single(nil) } } else { otherPeerName = .single(nil) @@ -2963,21 +2993,31 @@ private final class PremiumIntroScreenComponent: CombinedComponent { self.isPremium = true } + let isPremium: Signal + let promoConfiguration: Signal + switch screenContext { + case let .accountContext(context): + isPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> map { peer -> Bool in + return peer?.isPremium ?? false + } + promoConfiguration = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.PremiumPromo()) + case .sharedContext: + isPremium = .single(false) + promoConfiguration = .single(PremiumPromoConfiguration.defaultValue) + } + self.disposable = combineLatest( queue: Queue.mainQueue(), availableProducts, - context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.PremiumPromo()), - context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) - |> map { peer -> Bool in - return peer?.isPremium ?? false - }, + promoConfiguration, + isPremium, otherPeerName ).start(next: { [weak self] availableProducts, promoConfiguration, isPremium, otherPeerName in if let strongSelf = self { strongSelf.promoConfiguration = promoConfiguration let hadProducts = strongSelf.products != nil - var products: [PremiumProduct] = [] for option in promoConfiguration.premiumProductOptions { if let product = availableProducts.first(where: { $0.id == option.storeProductId }), product.isSubscription { @@ -2992,8 +3032,10 @@ private final class PremiumIntroScreenComponent: CombinedComponent { if !hadProducts { strongSelf.selectedProductId = strongSelf.products?.first?.id - for (_, video) in promoConfiguration.videos { - strongSelf.preloadDisposableSet.add(preloadVideoResource(postbox: context.account.postbox, userLocation: .other, userContentType: .video, resourceReference: .standalone(resource: video.resource), duration: 3.0).start()) + if let context = screenContext.context { + for (_, video) in promoConfiguration.videos { + strongSelf.preloadDisposableSet.add(preloadVideoResource(postbox: context.account.postbox, userLocation: .other, userContentType: .video, resourceReference: .standalone(resource: video.resource), duration: 3.0).start()) + } } } @@ -3007,14 +3049,16 @@ private final class PremiumIntroScreenComponent: CombinedComponent { self.emojiPackTitle = info.title self.updated(transition: .immediate) } else { - self.emojiFileDisposable = (context.engine.stickers.resolveInlineStickers(fileIds: [emojiFileId]) - |> deliverOnMainQueue).start(next: { [weak self] result in - guard let strongSelf = self else { - return - } - strongSelf.emojiFile = result[emojiFileId] - strongSelf.updated(transition: .immediate) - }) + if let context = screenContext.context { + self.emojiFileDisposable = (context.engine.stickers.resolveInlineStickers(fileIds: [emojiFileId]) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + self.emojiFile = result[emojiFileId] + self.updated(transition: .immediate) + }) + } } } } @@ -3032,12 +3076,17 @@ private final class PremiumIntroScreenComponent: CombinedComponent { return } + let presentationData = self.screenContext.presentationData + if case let .gift(_, _, _, giftCode) = self.source, let giftCode, giftCode.usedDate == nil { + guard let context = self.screenContext.context else { + return + } self.inProgress = true self.updateInProgress(true) self.updated(transition: .immediate) - self.paymentDisposable.set((self.context.engine.payments.applyPremiumGiftCode(slug: giftCode.slug) + self.paymentDisposable.set((context.engine.payments.applyPremiumGiftCode(slug: giftCode.slug) |> deliverOnMainQueue).start(error: { [weak self] error in guard let self else { return @@ -3048,8 +3097,6 @@ private final class PremiumIntroScreenComponent: CombinedComponent { self.updated(transition: .immediate) if case let .waitForExpiration(date) = error { - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let dateText = stringForMediumDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) self.present(UndoOverlayController(presentationData: presentationData, content: .info(title: presentationData.strings.Premium_Gift_ApplyLink_AlreadyHasPremium_Title, text: presentationData.strings.Premium_Gift_ApplyLink_AlreadyHasPremium_Text(dateText).string, timeout: nil, customUndoText: nil), elevatedLayout: true, position: .bottom, action: { _ in return true })) } @@ -3067,16 +3114,15 @@ private final class PremiumIntroScreenComponent: CombinedComponent { return } - guard let inAppPurchaseManager = self.context.inAppPurchaseManager, + guard let inAppPurchaseManager = self.screenContext.inAppPurchaseManager, let premiumProduct = self.products?.first(where: { $0.id == self.selectedProductId }) else { return } - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let isUpgrade = self.products?.first(where: { $0.isCurrent }) != nil var hasActiveSubsciption = false - if let data = self.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_receipt_check"] { + if let context = self.screenContext.context, let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_receipt_check"] { } else if !self.validPurchases.isEmpty && !isUpgrade { let now = Date() @@ -3089,26 +3135,40 @@ private final class PremiumIntroScreenComponent: CombinedComponent { if hasActiveSubsciption { let errorText = presentationData.strings.Premium_Purchase_OnlyOneSubscriptionAllowed - let alertController = textAlertController(context: self.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + let alertController = textAlertController(sharedContext: self.screenContext.sharedContext, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) self.present(alertController) return } - addAppLogEvent(postbox: self.context.account.postbox, type: "premium.promo_screen_accept") - + if let context = self.screenContext.context { + addAppLogEvent(postbox: context.account.postbox, type: "premium.promo_screen_accept") + } + self.inProgress = true self.updateInProgress(true) self.updated(transition: .immediate) let purpose: AppStoreTransactionPurpose = isUpgrade ? .upgrade : .subscription - let _ = (self.context.engine.payments.canPurchasePremium(purpose: purpose) + + let canPurchasePremium: Signal + switch self.screenContext { + case let .accountContext(context): + canPurchasePremium = context.engine.payments.canPurchasePremium(purpose: purpose) + case let .sharedContext(_, engine, _): + canPurchasePremium = engine.payments.canPurchasePremium(purpose: purpose) + } + let _ = (canPurchasePremium |> deliverOnMainQueue).start(next: { [weak self] available in - if let strongSelf = self { - if available { - strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct.storeProduct, purpose: purpose) - |> deliverOnMainQueue).start(next: { [weak self] status in - if let strongSelf = self, case .purchased = status { - strongSelf.activationDisposable.set((strongSelf.context.account.postbox.peerView(id: strongSelf.context.account.peerId) + guard let self else { + return + } + if available { + self.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct.storeProduct, purpose: purpose) + |> deliverOnMainQueue).start(next: { [weak self] status in + if let self, case .purchased = status { + let activation: Signal + if let context = self.screenContext.context { + activation = context.account.postbox.peerView(id: context.account.peerId) |> castError(AssignAppStoreTransactionError.self) |> take(until: { view in if let peer = view.peers[view.peerId], peer.isPremium { @@ -3121,70 +3181,82 @@ private final class PremiumIntroScreenComponent: CombinedComponent { return .never() } |> timeout(15.0, queue: Queue.mainQueue(), alternate: .fail(.timeout)) - |> deliverOnMainQueue).start(error: { [weak self] _ in - if let strongSelf = self { - strongSelf.inProgress = false - strongSelf.updateInProgress(false) - - strongSelf.updated(transition: .immediate) - - addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_fail") - - let errorText = presentationData.strings.Premium_Purchase_ErrorUnknown - let alertController = textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) - strongSelf.present(alertController) - } - }, completed: { [weak self] in - if let strongSelf = self { - let _ = updatePremiumPromoConfigurationOnce(account: strongSelf.context.account).start() - strongSelf.inProgress = false - strongSelf.updateInProgress(false) - - strongSelf.isPremium = true - strongSelf.justBought = true - - strongSelf.updated(transition: .easeInOut(duration: 0.25)) - strongSelf.completion() - } - })) + } else { + activation = .complete() } - }, error: { [weak self] error in - if let strongSelf = self { - strongSelf.inProgress = false - strongSelf.updateInProgress(false) - strongSelf.updated(transition: .immediate) - - var errorText: String? - switch error { - case .generic: - errorText = presentationData.strings.Premium_Purchase_ErrorUnknown - case .network: - errorText = presentationData.strings.Premium_Purchase_ErrorNetwork - case .notAllowed: - errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed - case .cantMakePayments: - errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments - case .assignFailed: - errorText = presentationData.strings.Premium_Purchase_ErrorUnknown - case .tryLater: - errorText = presentationData.strings.Premium_Purchase_ErrorUnknown - case .cancelled: - break - } - - if let errorText = errorText { - addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_fail") + + self.activationDisposable.set((activation + |> deliverOnMainQueue).start(error: { [weak self] _ in + if let self { + self.inProgress = false + self.updateInProgress(false) - let alertController = textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) - strongSelf.present(alertController) + self.updated(transition: .immediate) + + if let context = self.screenContext.context { + addAppLogEvent(postbox: context.account.postbox, type: "premium.promo_screen_fail") + } + + let errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + let alertController = textAlertController(sharedContext: self.screenContext.sharedContext, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + self.present(alertController) } + }, completed: { [weak self] in + guard let self else { + return + } + if let context = self.screenContext.context { + let _ = updatePremiumPromoConfigurationOnce(account: context.account).start() + } + self.inProgress = false + self.updateInProgress(false) + + self.isPremium = true + self.justBought = true + + self.updated(transition: .easeInOut(duration: 0.25)) + self.completion() + })) + } + }, error: { [weak self] error in + guard let self else { + return + } + self.inProgress = false + self.updateInProgress(false) + self.updated(transition: .immediate) + + var errorText: String? + switch error { + case .generic: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .network: + errorText = presentationData.strings.Premium_Purchase_ErrorNetwork + case .notAllowed: + errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed + case .cantMakePayments: + errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments + case .assignFailed: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .tryLater: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .cancelled: + break + } + + if let errorText = errorText { + if let context = self.screenContext.context { + addAppLogEvent(postbox: context.account.postbox, type: "premium.promo_screen_fail") } - })) - } else { - strongSelf.inProgress = false - strongSelf.updateInProgress(false) - strongSelf.updated(transition: .immediate) - } + + let alertController = textAlertController(sharedContext: self.screenContext.sharedContext, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + self.present(alertController) + } + })) + } else { + self.inProgress = false + self.updateInProgress(false) + self.updated(transition: .immediate) } }) } @@ -3201,7 +3273,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, source: self.source, forceHasPremium: self.forceHasPremium, updateInProgress: self.updateInProgress, present: self.present, completion: self.completion) + return State(screenContext: self.screenContext, source: self.source, forceHasPremium: self.forceHasPremium, updateInProgress: self.updateInProgress, present: self.present, completion: self.completion) } static var body: Body { @@ -3248,12 +3320,12 @@ private final class PremiumIntroScreenComponent: CombinedComponent { availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), transition: context.transition ) - } else if case let .emojiStatus(_, fileId, _, _) = context.component.source { + } else if case let .emojiStatus(_, fileId, _, _) = context.component.source, case let .accountContext(accountContext) = context.component.screenContext { header = emoji.update( component: EmojiHeaderComponent( - context: context.component.context, - animationCache: state.animationCache, - animationRenderer: state.animationRenderer, + context: accountContext, + animationCache: accountContext.animationCache, + animationRenderer: accountContext.animationRenderer, placeholderColor: environment.theme.list.mediaPlaceholderColor, accentColor: environment.theme.list.itemAccentColor, fileId: fileId, @@ -3356,7 +3428,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } else if case .profile = context.component.source { secondaryTitleText = environment.strings.Premium_PersonalTitle(otherPeerName).string } else if case let .gift(fromPeerId, _, duration, _) = context.component.source { - if fromPeerId == context.component.context.account.peerId { + if case let .accountContext(accountContext) = context.component.screenContext, fromPeerId == accountContext.account.peerId { if duration == 12 { secondaryTitleText = environment.strings.Premium_GiftedTitleYou_12Month(otherPeerName).string } else if duration == 6 { @@ -3409,14 +3481,13 @@ private final class PremiumIntroScreenComponent: CombinedComponent { secondaryAttributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: emojiFile.fileId.id, file: emojiFile), range: range) } } - let accountContext = context.component.context + let presentController = context.component.present - let secondaryTitle = secondaryTitle.update( component: MultilineTextWithEntitiesComponent( - context: context.component.context, - animationCache: context.state.animationCache, - animationRenderer: context.state.animationRenderer, + context: context.component.screenContext.context, + animationCache: context.component.screenContext.context?.animationCache, + animationRenderer: context.component.screenContext.context?.animationRenderer, placeholderColor: environment.theme.list.mediaPlaceholderColor, text: .plain(secondaryAttributedText), horizontalAlignment: .center, @@ -3431,7 +3502,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } } : nil, tapAction: { [weak state, weak environment] _, _ in - if let emojiFile = state?.emojiFile, let controller = environment?.controller() as? PremiumIntroScreen, let navigationController = controller.navigationController as? NavigationController { + if let emojiFile = state?.emojiFile, let controller = environment?.controller() as? PremiumIntroScreen, let context = controller.context, let navigationController = controller.navigationController as? NavigationController { for attribute in emojiFile.attributes { if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { var loadedPack: LoadedStickerPack? @@ -3439,7 +3510,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { loadedPack = .result(info: info, items: items, installed: updatedInstalled ?? installed) } - let controller = accountContext.sharedContext.makeStickerPackScreen(context: accountContext, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: loadedPack.flatMap { [$0] } ?? [], actionTitle: nil, isEditing: false, expandIfNeeded: false, parentNavigationController: navigationController, sendSticker: { _, _, _ in + let controller = context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: loadedPack.flatMap { [$0] } ?? [], actionTitle: nil, isEditing: false, expandIfNeeded: false, parentNavigationController: navigationController, sendSticker: { _, _, _ in return false }, actionPerformed: { added in updatedInstalled = added @@ -3462,7 +3533,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { let scrollContent = scrollContent.update( component: ScrollComponent( content: AnyComponent(PremiumIntroScreenContentComponent( - context: context.component.context, + screenContext: context.component.screenContext, mode: context.component.mode, source: context.component.source, forceDark: context.component.forceDark, @@ -3570,8 +3641,8 @@ private final class PremiumIntroScreenComponent: CombinedComponent { ) var isUnusedGift = false - if case let .gift(fromId, _, _, giftCode) = context.component.source { - if let giftCode, giftCode.usedDate == nil, fromId != context.component.context.account.peerId { + if case let .gift(fromId, _, _, giftCode) = context.component.source, let accountContext = context.component.screenContext.context { + if let giftCode, giftCode.usedDate == nil, fromId != accountContext.account.peerId { isUnusedGift = true } } @@ -3691,12 +3762,70 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } public final class PremiumIntroScreen: ViewControllerComponentContainer { + public enum ScreenContext { + case accountContext(AccountContext) + case sharedContext(SharedAccountContext, TelegramEngineUnauthorized, InAppPurchaseManager) + + var context: AccountContext? { + switch self { + case let .accountContext(context): + return context + case .sharedContext: + return nil + } + } + + var sharedContext: SharedAccountContext { + switch self { + case let .accountContext(context): + return context.sharedContext + case let .sharedContext(sharedContext, _, _): + return sharedContext + } + } + + var inAppPurchaseManager: InAppPurchaseManager? { + switch self { + case let .accountContext(context): + return context.inAppPurchaseManager + case let .sharedContext(_, _, inAppPurchaseManager): + return inAppPurchaseManager + } + } + + var presentationData: PresentationData { + switch self { + case let .accountContext(context): + return context.sharedContext.currentPresentationData.with { $0 } + case let .sharedContext(sharedContext, _, _): + return sharedContext.currentPresentationData.with { $0 } + } + } + + var updatedPresentationData: (initial: PresentationData, signal: Signal) { + switch self { + case let .accountContext(context): + return (initial: context.sharedContext.currentPresentationData.with { $0 }, signal: context.sharedContext.presentationData) + case let .sharedContext(sharedContext, _, _): + return (initial: sharedContext.currentPresentationData.with { $0 }, signal: sharedContext.presentationData) + } + } + } + public enum Mode { case premium case business } - fileprivate let context: AccountContext + fileprivate var context: AccountContext? { + switch self.screenContext { + case let .accountContext(context): + return context + case .sharedContext: + return nil + } + } + private let screenContext: ScreenContext fileprivate let mode: Mode private var didSetReady = false @@ -3709,18 +3838,24 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { public weak var containerView: UIView? public var animationColor: UIColor? - public init(context: AccountContext, mode: Mode = .premium, source: PremiumSource, modal: Bool = true, forceDark: Bool = false, forceHasPremium: Bool = false) { - self.context = context + public convenience init(context: AccountContext, mode: Mode = .premium, source: PremiumSource, modal: Bool = true, forceDark: Bool = false, forceHasPremium: Bool = false) { + self.init(screenContext: .accountContext(context), mode: mode, source: source, modal: modal, forceDark: forceDark, forceHasPremium: forceHasPremium) + } + + public init(screenContext: ScreenContext, mode: Mode = .premium, source: PremiumSource, modal: Bool = true, forceDark: Bool = false, forceHasPremium: Bool = false) { + self.screenContext = screenContext self.mode = mode - + + let presentationData = screenContext.presentationData + var updateInProgressImpl: ((Bool) -> Void)? var pushImpl: ((ViewController) -> Void)? var presentImpl: ((ViewController) -> Void)? var completionImpl: (() -> Void)? var copyLinkImpl: ((String) -> Void)? var shareLinkImpl: ((String) -> Void)? - super.init(context: context, component: PremiumIntroScreenComponent( - context: context, + super.init(component: PremiumIntroScreenComponent( + screenContext: screenContext, mode: mode, source: source, forceDark: forceDark, @@ -3743,10 +3878,8 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { shareLink: { link in shareLinkImpl?(link) } - ), navigationBarAppearance: .transparent, presentationMode: modal ? .modal : .default, theme: forceDark ? .dark : .default) - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - + ), navigationBarAppearance: .transparent, presentationMode: modal ? .modal : .default, theme: forceDark ? .dark : .default, updatedPresentationData: screenContext.updatedPresentationData) + if modal { let cancelItem = UIBarButtonItem(title: presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed)) self.navigationItem.setLeftBarButton(cancelItem, animated: false) @@ -3756,11 +3889,12 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { } updateInProgressImpl = { [weak self] inProgress in - if let strongSelf = self { - strongSelf.navigationItem.leftBarButtonItem?.isEnabled = !inProgress - strongSelf.view.disablesInteractiveTransitionGestureRecognizer = inProgress - strongSelf.view.disablesInteractiveModalDismiss = inProgress + guard let self else { + return } + self.navigationItem.leftBarButtonItem?.isEnabled = !inProgress + self.view.disablesInteractiveTransitionGestureRecognizer = inProgress + self.view.disablesInteractiveModalDismiss = inProgress } presentImpl = { [weak self] c in @@ -3789,12 +3923,11 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { } self.dismissAllTooltips() - let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, position: .top, action: { _ in return true }), in: .current) } shareLinkImpl = { [weak self] link in - guard let self, let navigationController = self.navigationController as? NavigationController else { + guard let self, case let .accountContext(context) = screenContext, let navigationController = self.navigationController as? NavigationController else { return } @@ -3807,7 +3940,6 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { HapticFeedback().success() } - let presentationData = context.sharedContext.currentPresentationData.with { $0 } (navigationController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: peer.id == context.account.peerId ? presentationData.strings.GiftLink_LinkSharedToSavedMessages : presentationData.strings.GiftLink_LinkSharedToChat(peer.compactDisplayTitle).string), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .window(.root)) let _ = (enqueueMessages(account: context.account, peerId: peer.id, messages: messages) @@ -3820,7 +3952,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { navigationController.pushViewController(peerSelectionController) } - if case .business = mode { + if case .business = mode, case let .accountContext(context) = screenContext { context.account.viewTracker.keepQuickRepliesApproximatelyUpdated() context.account.viewTracker.keepBusinessLinksApproximatelyUpdated() } diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift index bbf34ff84e..e686644d5c 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -16,6 +16,7 @@ import BundleIconComponent import Markdown import SolidRoundedButtonNode import BlurredBackgroundComponent +import PremiumCoinComponent public class PremiumLimitsListScreen: ViewController { final class Node: ViewControllerTracingNode, ASScrollViewDelegate, ASGestureRecognizerDelegate { diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index ddc57bf86d..4d7c541c9a 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -1220,7 +1220,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[957176926] = { return Api.auth.LoginToken.parse_loginTokenSuccess($0) } dict[326715557] = { return Api.auth.PasswordRecovery.parse_passwordRecovery($0) } dict[1577067778] = { return Api.auth.SentCode.parse_sentCode($0) } - dict[304435204] = { return Api.auth.SentCode.parse_sentCodePaymentRequired($0) } + dict[-674301568] = { return Api.auth.SentCode.parse_sentCodePaymentRequired($0) } dict[596704836] = { return Api.auth.SentCode.parse_sentCodeSuccess($0) } dict[1035688326] = { return Api.auth.SentCodeType.parse_sentCodeTypeApp($0) } dict[1398007207] = { return Api.auth.SentCodeType.parse_sentCodeTypeCall($0) } diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index a223f6f7dc..fe6078fe6d 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -567,7 +567,7 @@ public extension Api.auth { public extension Api.auth { enum SentCode: TypeConstructorDescription { case sentCode(flags: Int32, type: Api.auth.SentCodeType, phoneCodeHash: String, nextType: Api.auth.CodeType?, timeout: Int32?) - case sentCodePaymentRequired(storeProduct: String) + case sentCodePaymentRequired(storeProduct: String, phoneCodeHash: String) case sentCodeSuccess(authorization: Api.auth.Authorization) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -582,11 +582,12 @@ public extension Api.auth { if Int(flags) & Int(1 << 1) != 0 {nextType!.serialize(buffer, true)} if Int(flags) & Int(1 << 2) != 0 {serializeInt32(timeout!, buffer: buffer, boxed: false)} break - case .sentCodePaymentRequired(let storeProduct): + case .sentCodePaymentRequired(let storeProduct, let phoneCodeHash): if boxed { - buffer.appendInt32(304435204) + buffer.appendInt32(-674301568) } serializeString(storeProduct, buffer: buffer, boxed: false) + serializeString(phoneCodeHash, buffer: buffer, boxed: false) break case .sentCodeSuccess(let authorization): if boxed { @@ -601,8 +602,8 @@ public extension Api.auth { switch self { case .sentCode(let flags, let type, let phoneCodeHash, let nextType, let timeout): return ("sentCode", [("flags", flags as Any), ("type", type as Any), ("phoneCodeHash", phoneCodeHash as Any), ("nextType", nextType as Any), ("timeout", timeout as Any)]) - case .sentCodePaymentRequired(let storeProduct): - return ("sentCodePaymentRequired", [("storeProduct", storeProduct as Any)]) + case .sentCodePaymentRequired(let storeProduct, let phoneCodeHash): + return ("sentCodePaymentRequired", [("storeProduct", storeProduct as Any), ("phoneCodeHash", phoneCodeHash as Any)]) case .sentCodeSuccess(let authorization): return ("sentCodeSuccess", [("authorization", authorization as Any)]) } @@ -638,9 +639,12 @@ public extension Api.auth { public static func parse_sentCodePaymentRequired(_ reader: BufferReader) -> SentCode? { var _1: String? _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) let _c1 = _1 != nil - if _c1 { - return Api.auth.SentCode.sentCodePaymentRequired(storeProduct: _1!) + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.auth.SentCode.sentCodePaymentRequired(storeProduct: _1!, phoneCodeHash: _2!) } else { return nil diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index d1b1b90e71..3175e1573d 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -73,13 +73,13 @@ public class UnauthorizedAccount { public let testingEnvironment: Bool public let postbox: Postbox public let network: Network - private let stateManager: UnauthorizedAccountStateManager + let stateManager: UnauthorizedAccountStateManager private let updateLoginTokenPipe = ValuePipe() public var updateLoginTokenEvents: Signal { return self.updateLoginTokenPipe.signal() } - + private let serviceNotificationPipe = ValuePipe() public var serviceNotificationEvents: Signal { return self.serviceNotificationPipe.signal() @@ -91,7 +91,7 @@ public class UnauthorizedAccount { public let shouldBeServiceTaskMaster = Promise() - init(networkArguments: NetworkInitializationArguments, id: AccountRecordId, rootPath: String, basePath: String, testingEnvironment: Bool, postbox: Postbox, network: Network, shouldKeepAutoConnection: Bool = true) { + init(accountManager: AccountManager, networkArguments: NetworkInitializationArguments, id: AccountRecordId, rootPath: String, basePath: String, testingEnvironment: Bool, postbox: Postbox, network: Network, shouldKeepAutoConnection: Bool = true) { self.networkArguments = networkArguments self.id = id self.rootPath = rootPath @@ -101,11 +101,84 @@ public class UnauthorizedAccount { self.network = network let updateLoginTokenPipe = self.updateLoginTokenPipe let serviceNotificationPipe = self.serviceNotificationPipe - self.stateManager = UnauthorizedAccountStateManager(network: network, updateLoginToken: { - updateLoginTokenPipe.putNext(Void()) - }, displayServiceNotification: { text in - serviceNotificationPipe.putNext(text) - }) + let masterDatacenterId = Int32(network.mtProto.datacenterId) + + var updateSentCodeImpl: ((Api.auth.SentCode) -> Void)? + self.stateManager = UnauthorizedAccountStateManager( + network: network, + updateLoginToken: { + updateLoginTokenPipe.putNext(Void()) + }, + updateSentCode: { sentCode in + updateSentCodeImpl?(sentCode) + }, + displayServiceNotification: { text in + serviceNotificationPipe.putNext(text) + } + ) + + updateSentCodeImpl = { [weak self] sentCode in + switch sentCode { + case .sentCodePaymentRequired: + break + case let .sentCode(_, type, phoneCodeHash, nextType, codeTimeout): + let _ = postbox.transaction({ transaction in + var parsedNextType: AuthorizationCodeNextType? + if let nextType = nextType { + parsedNextType = AuthorizationCodeNextType(apiType: nextType) + } + if let state = transaction.getState() as? UnauthorizedAccountState, case let .payment(phoneNumber, _, _, syncContacts) = state.contents { + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: testingEnvironment, masterDatacenterId: masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: nil, usePrevious: false))) + } + }).start() + case let .sentCodeSuccess(authorization): + switch authorization { + case let .authorization(_, _, _, futureAuthToken, user): + let _ = postbox.transaction({ [weak self] transaction in + var syncContacts = true + if let state = transaction.getState() as? UnauthorizedAccountState, case let .payment(_, _, _, syncContactsValue) = state.contents { + syncContacts = syncContactsValue + } + + if let futureAuthToken = futureAuthToken { + storeFutureLoginToken(accountManager: accountManager, token: futureAuthToken.makeData()) + } + + let user = TelegramUser(user: user) + var isSupportUser = false + if let phone = user.phone, phone.hasPrefix("42"), phone.count <= 5 { + isSupportUser = true + } + let state = AuthorizedAccountState(isTestingEnvironment: testingEnvironment, masterDatacenterId: masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: []) + initializedAppSettingsAfterLogin(transaction: transaction, appVersion: networkArguments.appVersion, syncContacts: syncContacts) + transaction.setState(state) + return accountManager.transaction { [weak self] transaction -> SendAuthorizationCodeResult in + if let self { + switchToAuthorizedAccount(transaction: transaction, account: self, isSupportUser: isSupportUser) + } + return .loggedIn + } + }).start() + case let .authorizationSignUpRequired(_, termsOfService): + let _ = postbox.transaction({ [weak self] transaction in + if let self { + if let state = transaction.getState() as? UnauthorizedAccountState, case let .payment(number, codeHash, _, syncContacts) = state.contents { + let _ = beginSignUp( + account: self, + data: AuthorizationSignUpData( + number: number, + codeHash: codeHash, + code: .phoneCode(""), + termsOfService: termsOfService.flatMap(UnauthorizedAccountTermsOfService.init(apiTermsOfService:)), + syncContacts: syncContacts + ) + ).start() + } + } + }).start() + } + } + } network.shouldKeepConnection.set(self.shouldBeServiceTaskMaster.get() |> map { mode -> Bool in @@ -152,7 +225,7 @@ public class UnauthorizedAccount { |> mapToSignal { localizationSettings, proxySettings, networkSettings, appConfiguration -> Signal in return initializedNetwork(accountId: self.id, arguments: self.networkArguments, supplementary: false, datacenterId: Int(masterDatacenterId), keychain: keychain, basePath: self.basePath, testingEnvironment: self.testingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: nil, useRequestTimeoutTimers: false, appConfiguration: appConfiguration) |> map { network in - let updated = UnauthorizedAccount(networkArguments: self.networkArguments, id: self.id, rootPath: self.rootPath, basePath: self.basePath, testingEnvironment: self.testingEnvironment, postbox: self.postbox, network: network) + let updated = UnauthorizedAccount(accountManager: accountManager, networkArguments: self.networkArguments, id: self.id, rootPath: self.rootPath, basePath: self.basePath, testingEnvironment: self.testingEnvironment, postbox: self.postbox, network: network) updated.shouldBeServiceTaskMaster.set(self.shouldBeServiceTaskMaster.get()) return updated } @@ -250,7 +323,7 @@ public func accountWithId(accountManager: AccountManager map { network -> AccountResult in - return .unauthorized(UnauthorizedAccount(networkArguments: networkArguments, id: id, rootPath: rootPath, basePath: path, testingEnvironment: unauthorizedState.isTestingEnvironment, postbox: postbox, network: network, shouldKeepAutoConnection: shouldKeepAutoConnection)) + return .unauthorized(UnauthorizedAccount(accountManager: accountManager, networkArguments: networkArguments, id: id, rootPath: rootPath, basePath: path, testingEnvironment: unauthorizedState.isTestingEnvironment, postbox: postbox, network: network, shouldKeepAutoConnection: shouldKeepAutoConnection)) } case let authorizedState as AuthorizedAccountState: return postbox.transaction { transaction -> String? in @@ -269,7 +342,7 @@ public func accountWithId(accountManager: AccountManager map { network -> AccountResult in - return .unauthorized(UnauthorizedAccount(networkArguments: networkArguments, id: id, rootPath: rootPath, basePath: path, testingEnvironment: beginWithTestingEnvironment, postbox: postbox, network: network, shouldKeepAutoConnection: shouldKeepAutoConnection)) + return .unauthorized(UnauthorizedAccount(accountManager: accountManager, networkArguments: networkArguments, id: id, rootPath: rootPath, basePath: path, testingEnvironment: beginWithTestingEnvironment, postbox: postbox, network: network, shouldKeepAutoConnection: shouldKeepAutoConnection)) } } } diff --git a/submodules/TelegramCore/Sources/Authorization.swift b/submodules/TelegramCore/Sources/Authorization.swift index 40a363f57c..836687ce7f 100644 --- a/submodules/TelegramCore/Sources/Authorization.swift +++ b/submodules/TelegramCore/Sources/Authorization.swift @@ -356,9 +356,8 @@ public func sendAuthorizationCode(accountManager: AccountManager Void + private let updateSentCode: (Api.auth.SentCode) -> Void private let displayServiceNotification: (String) -> Void - init(network: Network, updateLoginToken: @escaping () -> Void, displayServiceNotification: @escaping (String) -> Void) { + init( + network: Network, + updateLoginToken: @escaping () -> Void, + updateSentCode: @escaping (Api.auth.SentCode) -> Void, + displayServiceNotification: @escaping (String) -> Void + ) { self.network = network self.updateLoginToken = updateLoginToken + self.updateSentCode = updateSentCode self.displayServiceNotification = displayServiceNotification } @@ -65,11 +72,18 @@ final class UnauthorizedAccountStateManager { self.updateServiceDisposable.dispose() } + func addUpdates(_ updates: Api.Updates) { + self.queue.async { + self.updateService?.addUpdates(updates) + } + } + func reset() { self.queue.async { if self.updateService == nil { self.updateService = UnauthorizedUpdateMessageService() let updateLoginToken = self.updateLoginToken + let updateSentCode = self.updateSentCode let displayServiceNotification = self.displayServiceNotification self.updateServiceDisposable.set(self.updateService!.pipe.signal().start(next: { updates in for update in updates { @@ -81,6 +95,8 @@ final class UnauthorizedAccountStateManager { if popup { displayServiceNotification(message) } + case let .updateSentPhoneCode(sentCode): + updateSentCode(sentCode) default: break } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift index 947985dc24..20b3ca184b 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift @@ -182,7 +182,7 @@ public indirect enum UnauthorizedAccountStateContents: PostboxCoding, Equatable case passwordRecovery(hint: String, number: String?, code: AuthorizationCode?, emailPattern: String, syncContacts: Bool) case awaitingAccountReset(protectedUntil: Int32, number: String?, syncContacts: Bool) case signUp(number: String, codeHash: String, firstName: String, lastName: String, termsOfService: UnauthorizedAccountTermsOfService?, syncContacts: Bool) - case payment(number: String, codeHash: String, storeProduct: String) + case payment(number: String, codeHash: String, storeProduct: String, syncContacts: Bool) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("v", orElse: 0) { @@ -216,9 +216,8 @@ public indirect enum UnauthorizedAccountStateContents: PostboxCoding, Equatable self = .awaitingAccountReset(protectedUntil: decoder.decodeInt32ForKey("protectedUntil", orElse: 0), number: decoder.decodeOptionalStringForKey("number"), syncContacts: decoder.decodeInt32ForKey("syncContacts", orElse: 1) != 0) case UnauthorizedAccountStateContentsValue.signUp.rawValue: self = .signUp(number: decoder.decodeStringForKey("n", orElse: ""), codeHash: decoder.decodeStringForKey("h", orElse: ""), firstName: decoder.decodeStringForKey("f", orElse: ""), lastName: decoder.decodeStringForKey("l", orElse: ""), termsOfService: decoder.decodeObjectForKey("tos", decoder: { UnauthorizedAccountTermsOfService(decoder: $0) }) as? UnauthorizedAccountTermsOfService, syncContacts: decoder.decodeInt32ForKey("syncContacts", orElse: 1) != 0) - case UnauthorizedAccountStateContentsValue.payment.rawValue: - self = .payment(number: decoder.decodeStringForKey("n", orElse: ""), codeHash: decoder.decodeStringForKey("h", orElse: ""), storeProduct: decoder.decodeStringForKey("storeProduct", orElse: "")) + self = .payment(number: decoder.decodeStringForKey("n", orElse: ""), codeHash: decoder.decodeStringForKey("h", orElse: ""), storeProduct: decoder.decodeStringForKey("storeProduct", orElse: ""), syncContacts: decoder.decodeInt32ForKey("syncContacts", orElse: 1) != 0) default: assertionFailure() self = .empty @@ -308,11 +307,12 @@ public indirect enum UnauthorizedAccountStateContents: PostboxCoding, Equatable encoder.encodeNil(forKey: "tos") } encoder.encodeInt32(syncContacts ? 1 : 0, forKey: "syncContacts") - case let .payment(number, codeHash, storeProduct): + case let .payment(number, codeHash, storeProduct, syncContacts): encoder.encodeInt32(UnauthorizedAccountStateContentsValue.payment.rawValue, forKey: "v") encoder.encodeString(number, forKey: "n") encoder.encodeString(codeHash, forKey: "h") encoder.encodeString(storeProduct, forKey: "storeProduct") + encoder.encodeInt32(syncContacts ? 1 : 0, forKey: "syncContacts") } } @@ -384,8 +384,8 @@ public indirect enum UnauthorizedAccountStateContents: PostboxCoding, Equatable } else { return false } - case let .payment(number, codeHash, storeProduct): - if case .payment(number, codeHash, storeProduct) = rhs { + case let .payment(number, codeHash, storeProduct, syncContacts): + if case .payment(number, codeHash, storeProduct, syncContacts) = rhs { return true } else { return false diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift index 68f0cf550a..c89af92e93 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift @@ -148,7 +148,7 @@ private func apiInputStorePaymentPurpose(postbox: Postbox, purpose: AppStoreTran } } -func _internal_sendAppStoreReceipt(postbox: Postbox, network: Network, stateManager: AccountStateManager?, receipt: Data, purpose: AppStoreTransactionPurpose) -> Signal { +func _internal_sendAppStoreReceipt(postbox: Postbox, network: Network, stateManager: AccountStateManager, receipt: Data, purpose: AppStoreTransactionPurpose) -> Signal { return apiInputStorePaymentPurpose(postbox: postbox, purpose: purpose) |> castError(AssignAppStoreTransactionError.self) |> mapToSignal { purpose -> Signal in @@ -161,7 +161,26 @@ func _internal_sendAppStoreReceipt(postbox: Postbox, network: Network, stateMana } } |> mapToSignal { updates -> Signal in - stateManager?.addUpdates(updates) + stateManager.addUpdates(updates) + return .complete() + } + } +} + +func _internal_sendAppStoreReceipt(postbox: Postbox, network: Network, stateManager: UnauthorizedAccountStateManager, receipt: Data, purpose: AppStoreTransactionPurpose) -> Signal { + return apiInputStorePaymentPurpose(postbox: postbox, purpose: purpose) + |> castError(AssignAppStoreTransactionError.self) + |> mapToSignal { purpose -> Signal in + return network.request(Api.functions.payments.assignAppStoreTransaction(receipt: Buffer(data: receipt), purpose: purpose)) + |> mapError { error -> AssignAppStoreTransactionError in + if error.errorCode == 406 { + return .serverProvided + } else { + return .generic + } + } + |> mapToSignal { updates -> Signal in + stateManager.addUpdates(updates) return .complete() } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index c26d11bc60..7c1a697456 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -164,7 +164,7 @@ public extension TelegramEngineUnauthorized { } public func sendAppStoreReceipt(receipt: Data, purpose: AppStoreTransactionPurpose) -> Signal { - return _internal_sendAppStoreReceipt(postbox: self.account.postbox, network: self.account.network, stateManager: nil, receipt: receipt, purpose: purpose) + return _internal_sendAppStoreReceipt(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, receipt: receipt, purpose: purpose) } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift index be901426e8..22ee8094e3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift @@ -1213,9 +1213,9 @@ public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatE private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? - private let stars: StarsAmount? + private let stars: Int64? - public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?, stars: StarsAmount?) { + public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?, stars: Int64?) { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) self.isPremiumDisabled = premiumConfiguration.isPremiumDisabled self.stars = stars @@ -1295,7 +1295,7 @@ public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatE } ) if let amount = self.stars { - let starsString = presentationStringsFormattedNumber(Int32(amount.value), interfaceState.dateTimeFormat.groupingSeparator) + let starsString = presentationStringsFormattedNumber(Int32(amount), interfaceState.dateTimeFormat.groupingSeparator) let rawText: String if self.isPremiumDisabled { rawText = interfaceState.strings.Chat_EmptyStatePaidMessagingDisabled_Text(peerTitle, " $ \(starsString)").string @@ -1426,7 +1426,7 @@ private enum ChatEmptyNodeContentType: Equatable { case greeting case topic case premiumRequired - case starsRequired + case starsRequired(Int64) } private final class EmptyAttachedDescriptionNode: HighlightTrackingButtonNode { @@ -1815,8 +1815,8 @@ public final class ChatEmptyNode: ASDisplayNode { } else if let _ = interfaceState.peerNearbyData { contentType = .peerNearby } else if let peer = peer as? TelegramUser { - if let _ = interfaceState.sendPaidMessageStars, interfaceState.businessIntro == nil { - contentType = .starsRequired + if let sendPaidMessageStars = interfaceState.sendPaidMessageStars, interfaceState.businessIntro == nil { + contentType = .starsRequired(sendPaidMessageStars.value) } else if interfaceState.isPremiumRequiredForMessaging { contentType = .premiumRequired } else { @@ -1881,8 +1881,8 @@ public final class ChatEmptyNode: ASDisplayNode { node = ChatEmptyNodeTopicChatContent(context: self.context) case .premiumRequired: node = ChatEmptyNodePremiumRequiredChatContent(context: self.context, interaction: self.interaction, stars: nil) - case .starsRequired: - node = ChatEmptyNodePremiumRequiredChatContent(context: self.context, interaction: self.interaction, stars: interfaceState.sendPaidMessageStars) + case let .starsRequired(stars): + node = ChatEmptyNodePremiumRequiredChatContent(context: self.context, interaction: self.interaction, stars: stars) } self.content = (contentType, node) self.addSubnode(node) @@ -1893,7 +1893,12 @@ public final class ChatEmptyNode: ASDisplayNode { node.layer.animateScale(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) } } - self.isUserInteractionEnabled = [.peerNearby, .greeting, .premiumRequired, .starsRequired, .cloud].contains(contentType) + switch contentType { + case .peerNearby, .greeting, .premiumRequired, .starsRequired, .cloud: + self.isUserInteractionEnabled = true + default: + self.isUserInteractionEnabled = false + } let displayRect = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom)) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 1064b9d35a..d967016c40 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1044,6 +1044,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var edited = false var viewCount: Int? = nil var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -1057,6 +1058,8 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -1086,6 +1089,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { areReactionsTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: messageEffect, replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.message), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 297d1a9980..2df108a4e8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -675,6 +675,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: context.account.peerId, accountPeer: associatedData.accountPeer, message: message) if message.isRestricted(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) || presentationData.isPreview { dateReactionsAndPeers = ([], []) @@ -688,6 +689,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -747,6 +750,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { areReactionsTags: message.areReactionsTags(accountPeerId: context.account.peerId), messageEffect: message.messageEffect(availableMessageEffects: associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: message), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index eee6a16b63..6eb157593b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -130,7 +130,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ result.append((message, ChatMessageRestrictedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false break outer - } else if let _ = attribute as? PaidStarsMessageAttribute, !addedPriceInfo { + } else if let _ = attribute as? PaidStarsMessageAttribute, !addedPriceInfo, message.id.peerId.namespace == Namespaces.Peer.CloudUser { result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) addedPriceInfo = true } @@ -2276,6 +2276,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: message) if message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -2289,6 +2290,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -2337,6 +2340,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI areReactionsTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.message.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: message), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift index e6156462fc..a64006553e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift @@ -233,6 +233,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -246,6 +247,8 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -300,6 +303,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: messageEffect, replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift index 7665ed8d47..eca913ad51 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift @@ -195,6 +195,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { var areReactionsTags: Bool var messageEffect: AvailableMessageEffects.MessageEffect? var replyCount: Int + var starsCount: Int64? var isPinned: Bool var hasAutoremove: Bool var canViewReactionList: Bool @@ -218,6 +219,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { areReactionsTags: Bool, messageEffect: AvailableMessageEffects.MessageEffect?, replyCount: Int, + starsCount: Int64?, isPinned: Bool, hasAutoremove: Bool, canViewReactionList: Bool, @@ -240,6 +242,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { self.areReactionsTags = areReactionsTags self.messageEffect = messageEffect self.replyCount = replyCount + self.starsCount = starsCount self.isPinned = isPinned self.hasAutoremove = hasAutoremove self.canViewReactionList = canViewReactionList @@ -262,6 +265,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { private var repliesIcon: ASImageNode? private var selfExpiringIcon: ASImageNode? private var replyCountNode: TextNode? + private var starsIcon: ASImageNode? + private var starsCountNode: TextNode? private var type: ChatMessageDateAndStatusType? private var theme: ChatPresentationThemeData? @@ -316,11 +321,13 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { var currentBackgroundNode = self.backgroundNode var currentImpressionIcon = self.impressionIcon var currentRepliesIcon = self.repliesIcon + var currentStarsIcon = self.starsIcon let currentType = self.type let currentTheme = self.theme let makeReplyCountLayout = TextNode.asyncLayout(self.replyCountNode) + let makeStarsCountLayout = TextNode.asyncLayout(self.starsCountNode) let reactionButtonsContainer = self.reactionButtonsContainer @@ -337,6 +344,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { let clockMinImage: UIImage? var impressionImage: UIImage? var repliesImage: UIImage? + var starsImage: UIImage? let themeUpdated = arguments.presentationData.theme != currentTheme || arguments.type != currentType @@ -404,6 +412,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } else if arguments.isPinned { repliesImage = graphics.incomingDateAndStatusPinnedIcon } + if (arguments.starsCount ?? 0) != 0 { + starsImage = graphics.incomingDateAndStatusRepliesIcon + } case let .BubbleOutgoing(status): dateColor = arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor outgoingStatus = status @@ -420,6 +431,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } else if arguments.isPinned { repliesImage = graphics.outgoingDateAndStatusPinnedIcon } + if (arguments.starsCount ?? 0) != 0 { + starsImage = graphics.outgoingDateAndStatusRepliesIcon + } case .ImageIncoming: dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor backgroundImage = graphics.dateAndStatusMediaBackground @@ -436,6 +450,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } else if arguments.isPinned { repliesImage = graphics.mediaPinnedIcon } + if (arguments.starsCount ?? 0) != 0 { + starsImage = graphics.mediaRepliesIcon + } case let .ImageOutgoing(status): dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor outgoingStatus = status @@ -453,6 +470,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } else if arguments.isPinned { repliesImage = graphics.mediaPinnedIcon } + if (arguments.starsCount ?? 0) != 0 { + starsImage = graphics.mediaRepliesIcon + } case .FreeIncoming: let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper) dateColor = serviceColor.primaryText @@ -471,6 +491,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } else if arguments.isPinned { repliesImage = graphics.freePinnedIcon } + if (arguments.starsCount ?? 0) != 0 { + starsImage = graphics.freeRepliesIcon + } case let .FreeOutgoing(status): let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper) dateColor = serviceColor.primaryText @@ -489,6 +512,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } else if arguments.isPinned { repliesImage = graphics.freePinnedIcon } + if (arguments.starsCount ?? 0) != 0 { + starsImage = graphics.freeRepliesIcon + } } var updatedDateText = arguments.dateText @@ -541,6 +567,20 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { currentRepliesIcon = nil } + var starsIconSize = CGSize() + if let starsImage = starsImage { + if currentStarsIcon == nil { + let iconNode = ASImageNode() + iconNode.isLayerBacked = true + iconNode.displayWithoutProcessing = true + iconNode.displaysAsynchronously = false + currentStarsIcon = iconNode + } + starsIconSize = starsImage.size + } else { + currentStarsIcon = nil + } + if let outgoingStatus = outgoingStatus { switch outgoingStatus { case .Sending: @@ -652,6 +692,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } var replyCountLayoutAndApply: (TextNodeLayout, () -> TextNode)? + var starsCountLayoutAndApply: (TextNodeLayout, () -> TextNode)? let reactionSize: CGFloat = 8.0 let reactionSpacing: CGFloat = 2.0 @@ -676,6 +717,21 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { reactionInset += 12.0 } + if let starsCount = arguments.starsCount, starsCount > 0 { + let countString: String + if starsCount > 1000000 { + countString = "\(starsCount / 1000000)M" + } else if starsCount > 1000 { + countString = "\(starsCount / 1000)K" + } else { + countString = "\(starsCount)" + } + + let layoutAndApply = makeStarsCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0))) + reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0 + starsCountLayoutAndApply = layoutAndApply + } + if arguments.messageEffect != nil { reactionInset += 13.0 } @@ -1237,6 +1293,56 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { replyCountNode.removeFromSupernode() } } + + if let currentStarsIcon = currentStarsIcon { + currentStarsIcon.displaysAsynchronously = false + if currentStarsIcon.image !== starsImage { + currentStarsIcon.image = starsImage + } + if currentStarsIcon.supernode == nil { + strongSelf.starsIcon = currentStarsIcon + strongSelf.addSubnode(currentStarsIcon) + if animation.isAnimated { + currentStarsIcon.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + } + let starsIconFrame = CGRect(origin: CGPoint(x: reactionOffset - 2.0, y: backgroundInsets.top + offset + verticalInset + floor((date.size.height - starsIconSize.height) / 2.0)), size: starsIconSize) + animation.animator.updateFrame(layer: currentStarsIcon.layer, frame: starsIconFrame, completion: nil) + reactionOffset += 9.0 + } else if let starsIcon = strongSelf.starsIcon { + strongSelf.starsIcon = nil + if animation.isAnimated { + starsIcon.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak starsIcon] _ in + starsIcon?.removeFromSupernode() + }) + } else { + starsIcon.removeFromSupernode() + } + } + + if let (layout, apply) = starsCountLayoutAndApply { + let node = apply() + if strongSelf.starsCountNode !== node { + strongSelf.starsCountNode?.removeFromSupernode() + strongSelf.addSubnode(node) + strongSelf.starsCountNode = node + if animation.isAnimated { + node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + } + let starsCountFrame = CGRect(origin: CGPoint(x: reactionOffset + 4.0, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: layout.size) + animation.animator.updateFrame(layer: node.layer, frame: starsCountFrame, completion: nil) + reactionOffset += 4.0 + layout.size.width + } else if let starsCountNode = strongSelf.starsCountNode { + strongSelf.starsCountNode = nil + if animation.isAnimated { + starsCountNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak starsCountNode] _ in + starsCountNode?.removeFromSupernode() + }) + } else { + starsCountNode.removeFromSupernode() + } + } } }) }) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift index a2e2b5deef..a41e7a8686 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift @@ -295,6 +295,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode var rawText = "" var rawEntities: [MessageTextEntity] = [] var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -311,6 +312,8 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -447,6 +450,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift index e939dc18e6..263522c344 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift @@ -578,6 +578,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode, areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: nil, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index 5357300699..88c6dd9b03 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -898,6 +898,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: arguments.context.account.peerId, accountPeer: arguments.associatedData.accountPeer, message: arguments.topMessage) if arguments.topMessage.isRestricted(platform: "ios", contentSettings: arguments.context.currentContentSettings.with { $0 }) || arguments.presentationData.isPreview { dateReactionsAndPeers = ([], []) @@ -911,6 +912,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { if let channel = arguments.message.peers[arguments.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, arguments.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } if arguments.forcedIsEdited { @@ -956,6 +959,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { areReactionsTags: arguments.message.areReactionsTags(accountPeerId: arguments.context.account.peerId), messageEffect: arguments.message.messageEffect(availableMessageEffects: arguments.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: arguments.isPinned && !arguments.associatedData.isInPinnedListMode, hasAutoremove: arguments.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: arguments.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index a55f886df6..f95da1047c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -524,6 +524,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { let sentViaBot = false var viewCount: Int? = nil var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -537,6 +538,8 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -583,6 +586,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: messageEffect, replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 5946804836..468ea63fdf 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -83,6 +83,7 @@ public struct ChatMessageDateAndStatus { public var dateReactions: [MessageReaction] public var dateReactionPeers: [(MessageReaction.Reaction, EnginePeer)] public var dateReplies: Int + public var starsCount: Int64? public var isPinned: Bool public var dateText: String @@ -93,6 +94,7 @@ public struct ChatMessageDateAndStatus { dateReactions: [MessageReaction], dateReactionPeers: [(MessageReaction.Reaction, EnginePeer)], dateReplies: Int, + starsCount: Int64?, isPinned: Bool, dateText: String ) { @@ -102,6 +104,7 @@ public struct ChatMessageDateAndStatus { self.dateReactions = dateReactions self.dateReactionPeers = dateReactionPeers self.dateReplies = dateReplies + self.starsCount = starsCount self.isPinned = isPinned self.dateText = dateText } @@ -1118,6 +1121,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr areReactionsTags: message.areReactionsTags(accountPeerId: context.account.peerId), messageEffect: messageEffect, replyCount: dateAndStatus.dateReplies, + starsCount: dateAndStatus.starsCount, isPinned: dateAndStatus.isPinned, hasAutoremove: message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: message), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift index 2c8c18860a..a9511b99c1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift @@ -192,6 +192,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -205,6 +206,8 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -284,6 +287,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift index 49392be769..8f8397d26e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift @@ -307,6 +307,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -323,6 +324,8 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -373,6 +376,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { dateReactions: dateReactionsAndPeers.reactions, dateReactionPeers: dateReactionsAndPeers.peers, dateReplies: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, dateText: dateText ) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index 332531f153..0017cedcf4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -1054,6 +1054,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -1067,6 +1068,8 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -1125,6 +1128,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift index 8d5e633293..01a336ad60 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -56,6 +56,7 @@ public class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNod var viewCount: Int? var rawText = "" var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -71,6 +72,8 @@ public class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNod if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -140,6 +143,7 @@ public class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNod areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index c968aa1a50..e2d44addfe 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -602,6 +602,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { var edited = false var viewCount: Int? = nil var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -615,6 +616,8 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -648,6 +651,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { areReactionsTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.message.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.message), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index f6e820a9af..1e0dd952d7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -264,6 +264,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.topMessage) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -278,6 +279,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -647,6 +650,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && (!item.associatedData.isInPinnedListMode || isReplyThread), hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/BUILD index a5ce7cf0f9..7f66e412c3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/BUILD @@ -28,7 +28,6 @@ swift_library( "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListItemSliderSelectorComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", - "//submodules/PremiumUI", "//submodules/Components/BlurredBackgroundComponent", "//submodules/Markdown", "//submodules/PresentationDataUtils", @@ -38,6 +37,7 @@ swift_library( "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/ToastComponent", "//submodules/AvatarNode", + "//submodules/TelegramUI/Components/Premium/PremiumCoinComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift index 7a48e4fd5f..c27262b53c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift @@ -19,13 +19,13 @@ import ListItemSliderSelectorComponent import ListActionItemComponent import Markdown import BlurredBackgroundComponent -import PremiumUI import PresentationDataUtils import PeerListItemComponent import TelegramStringFormatting import ContextUI import BalancedTextComponent import AlertComponent +import PremiumCoinComponent private func textForTimeout(value: Int32) -> String { if value < 3600 { diff --git a/submodules/TelegramUI/Components/Premium/PremiumCoinComponent/BUILD b/submodules/TelegramUI/Components/Premium/PremiumCoinComponent/BUILD new file mode 100644 index 0000000000..faac7b084f --- /dev/null +++ b/submodules/TelegramUI/Components/Premium/PremiumCoinComponent/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PremiumCoinComponent", + module_name = "PremiumCoinComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/GZip", + "//submodules/LegacyComponents", + "//submodules/Components/MultilineTextComponent:MultilineTextComponent", + "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/PremiumUI/Sources/PremiumCoinComponent.swift b/submodules/TelegramUI/Components/Premium/PremiumCoinComponent/Sources/PremiumCoinComponent.swift similarity index 99% rename from submodules/PremiumUI/Sources/PremiumCoinComponent.swift rename to submodules/TelegramUI/Components/Premium/PremiumCoinComponent/Sources/PremiumCoinComponent.swift index 3a4a434cb6..866f24c1c5 100644 --- a/submodules/PremiumUI/Sources/PremiumCoinComponent.swift +++ b/submodules/TelegramUI/Components/Premium/PremiumCoinComponent/Sources/PremiumCoinComponent.swift @@ -48,6 +48,9 @@ public final class PremiumCoinComponent: Component { public final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView { public final class Tag { + public init() { + + } } public func matches(tag: Any) -> Bool { @@ -58,7 +61,7 @@ public final class PremiumCoinComponent: Component { } private var _ready = Promise() - var ready: Signal { + public var ready: Signal { return self._ready.get() } diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Contents.json new file mode 100644 index 0000000000..6e965652df --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Cost.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Cost.imageset/Contents.json new file mode 100644 index 0000000000..9d73b22267 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Cost.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "highprice_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Cost.imageset/highprice_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Cost.imageset/highprice_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..68a0449411a0a8d2ba09adf84a4ea0a7c9d03eeb GIT binary patch literal 4940 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Verification.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Verification.imageset/Contents.json new file mode 100644 index 0000000000..d9c134477a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Verification.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "verificationcode_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Verification.imageset/verificationcode_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Verification.imageset/verificationcode_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..70e1d19c8cacb2fd9f14b4842f8d8c81d33b03a2 GIT binary patch literal 4908 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 9968f1e98b..65c74b7751 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2366,13 +2366,11 @@ public final class SharedAccountContextImpl: SharedAccountContext { } } - public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController { - var modal = true + private func mapIntroSource(source: PremiumIntroSource) -> PremiumSource { let mappedSource: PremiumSource switch source { case .settings: mappedSource = .settings - modal = false case .stickers: mappedSource = .stickers case .reactions: @@ -2454,7 +2452,25 @@ public final class SharedAccountContextImpl: SharedAccountContext { case .paidMessages: mappedSource = .paidMessages } - let controller = PremiumIntroScreen(context: context, source: mappedSource, modal: modal, forceDark: forceDark) + return mappedSource + } + + public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController { + var modal = true + if case .settings = source { + modal = false + } + let controller = PremiumIntroScreen(context: context, source: self.mapIntroSource(source: source), modal: modal, forceDark: forceDark) + controller.wasDismissed = dismissed + return controller + } + + public func makePremiumIntroController(sharedContext: SharedAccountContext, engine: TelegramEngineUnauthorized, inAppPurchaseManager: InAppPurchaseManager, source: PremiumIntroSource, dismissed: (() -> Void)?) -> ViewController { + var modal = true + if case .settings = source { + modal = false + } + let controller = PremiumIntroScreen(screenContext: .sharedContext(sharedContext, engine, inAppPurchaseManager), source: self.mapIntroSource(source: source), modal: modal) controller.wasDismissed = dismissed return controller } From bafbe200634a30520e01963fe1a5220c7ba70adc Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 11 Mar 2025 18:48:35 +0400 Subject: [PATCH 14/19] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 10 ++ .../Sources/AccountContext.swift | 3 +- .../AccountContext/Sources/Premium.swift | 1 + .../AuthorizationSequencePaymentScreen.swift | 20 ++-- .../Sources/ChatListController.swift | 12 +++ .../Sources/SolidRoundedButtonComponent.swift | 7 ++ .../Sources/ViewControllerComponent.swift | 1 + .../LegacyComponents/TGMediaEditingContext.h | 2 - .../Sources/PremiumIntroScreen.swift | 62 ++++++++--- ...ectivePrivacySettingsPeersController.swift | 2 +- .../Sources/SolidRoundedButtonNode.swift | 8 +- submodules/TelegramApi/Sources/Api0.swift | 2 + submodules/TelegramApi/Sources/Api15.swift | 44 ++++++++ .../ApiUtils/StoreMessage_Telegram.swift | 2 +- .../ApiUtils/TelegramMediaAction.swift | 4 + .../SyncCore_TelegramMediaAction.swift | 13 +++ .../TelegramEngine/Messages/SendAsPeers.swift | 9 +- .../PresentationThemeEssentialGraphics.swift | 18 ++++ .../Sources/ServiceMessageStrings.swift | 27 +++++ .../ChatMessageDateAndStatusNode.swift | 12 +-- .../Sources/PeerInfoCoverComponent.swift | 2 +- .../Sources/PeerInfoGiftsCoverComponent.swift | 98 +++++++++++------- .../Sources/PeerInfoHeaderNode.swift | 1 + .../Sources/AccountFreezeInfoScreen.swift | 12 +-- .../Appeal.imageset/Contents.json | 2 +- .../sandtimer_30.pdf | Bin .../Violation.imageset/Contents.json | 2 +- .../hand_30.pdf | Bin .../Message/StarsCount.imageset/Contents.json | 12 +++ .../Message/StarsCount.imageset/msgstar.pdf | Bin 0 -> 4264 bytes .../TelegramUI/Sources/AccountContext.swift | 17 +++ .../Chat/ChatControllerFrozenAccount.swift | 32 ++++++ .../Chat/ChatControllerLoadDisplayNode.swift | 18 +++- ...ChatControllerOpenMessageContextMenu.swift | 8 ++ .../TelegramUI/Sources/ChatController.swift | 71 +++++++------ .../ChatInterfaceStateInputPanels.swift | 27 +++-- .../Sources/ChatTextInputPanelNode.swift | 23 +--- .../Sources/SharedAccountContext.swift | 6 +- 38 files changed, 434 insertions(+), 156 deletions(-) rename submodules/TelegramUI/Images.xcassets/Account Freeze/{Violation.imageset => Appeal.imageset}/sandtimer_30.pdf (100%) rename submodules/TelegramUI/Images.xcassets/Account Freeze/{Appeal.imageset => Violation.imageset}/hand_30.pdf (100%) create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/StarsCount.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/StarsCount.imageset/msgstar.pdf create mode 100644 submodules/TelegramUI/Sources/Chat/ChatControllerFrozenAccount.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 272447cd7e..c232c208da 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13996,3 +13996,13 @@ Sorry for the inconvenience."; "Stars.AccountRevenue.Proceeds.Info" = "Stars from your total balance can be withdrawn as rewards 21 days after they are earned."; "Stars.AccountRevenue.Withdraw.Info" = "You can collect rewards for Stars using Fragment. You cannot withdraw less than 1000 stars. [Learn More >]()"; + +"Notification.PaidMessageRefund.Stars_1" = "%@ Star"; +"Notification.PaidMessageRefund.Stars_any" = "%@ Stars"; +"Notification.PaidMessageRefund" = "%1$@ refunded you %2$@"; +"Notification.PaidMessageRefundYou" = "You refunded %1$@ to %2$@"; + +"Notification.PaidMessagePriceChanged.Stars_1" = "%@ Star"; +"Notification.PaidMessagePriceChanged.Stars_any" = "%@ Stars"; +"Notification.PaidMessagePriceChanged" = "%1$@ changed price to %2$@ per message"; +"Notification.PaidMessagePriceChangedYou" = "You changed price to %1$@ per message"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index aaa6ae47f3..958b1beef7 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1053,7 +1053,7 @@ public protocol SharedAccountContext: AnyObject { func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?, temporary: Bool) -> ViewController func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController - func makePremiumIntroController(sharedContext: SharedAccountContext, engine: TelegramEngineUnauthorized, inAppPurchaseManager: InAppPurchaseManager, source: PremiumIntroSource, dismissed: (() -> Void)?) -> ViewController + func makePremiumIntroController(sharedContext: SharedAccountContext, engine: TelegramEngineUnauthorized, inAppPurchaseManager: InAppPurchaseManager, source: PremiumIntroSource, proceed: (() -> Void)?) -> ViewController func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, forceDark: Bool, action: @escaping () -> Void, dismissed: (() -> Void)?) -> ViewController func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController @@ -1232,6 +1232,7 @@ public protocol AccountContext: AnyObject { var availableMessageEffects: Signal { get } var isPremium: Bool { get } + var isFrozen: Bool { get } var userLimits: EngineConfiguration.UserLimits { get } var peerNameColors: PeerNameColors { get } diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 86a92800d1..cccec77d59 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -43,6 +43,7 @@ public enum PremiumIntroSource { case animatedEmoji case messageEffects case paidMessages + case auth(String) } public enum PremiumGiftSource: Equatable { diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift index 914bfb8934..51354a786d 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift @@ -251,15 +251,17 @@ final class AuthorizationSequencePaymentScreenComponent: Component { iconName: "Premium/Authorization/Support", iconColor: linkColor, action: { [weak self] in - guard let self, let controller = self.environment?.controller() else { + guard let self, let controller = self.environment?.controller(), let product = self.products.first(where: { $0.id == component.storeProduct }) else { return } let introController = component.sharedContext.makePremiumIntroController( sharedContext: component.sharedContext, engine: component.engine, inAppPurchaseManager: component.inAppPurchaseManager, - source: .about, - dismissed: nil + source: .auth(product.price), + proceed: { [weak self] in + self?.proceed() + } ) controller.push(introController) } @@ -274,11 +276,13 @@ final class AuthorizationSequencePaymentScreenComponent: Component { containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) ) + let buttonHeight: CGFloat = 50.0 + let bottomPanelPadding: CGFloat = 12.0 let titleSpacing: CGFloat = -24.0 let listSpacing: CGFloat = 12.0 let totalHeight = animationSize.height + titleSpacing + titleSize.height + listSpacing + listSize.height - var originY = floor((availableSize.height - totalHeight) / 2.0) + var originY = floor((availableSize.height - buttonHeight - bottomPanelPadding * 2.0 - totalHeight) / 2.0) if let animationView = self.animation.view { if animationView.superview == nil { @@ -302,8 +306,6 @@ final class AuthorizationSequencePaymentScreenComponent: Component { listView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - listSize.width) / 2.0), y: originY), size: listSize) } - let buttonHeight: CGFloat = 50.0 - let bottomPanelPadding: CGFloat = 12.0 let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset @@ -329,7 +331,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component { component: AnyComponent( VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))), - AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Get Telegram Premium for 1 week", font: Font.regular(11.0), textColor: environment.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), paragraphAlignment: .center))))) + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Get Telegram Premium for 1 week", font: Font.medium(11.0), textColor: environment.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), paragraphAlignment: .center))))) ], spacing: 1.0) ) ), @@ -410,6 +412,10 @@ public final class AuthorizationSequencePaymentScreen: ViewControllerComponentCo fatalError("init(coder:) has not been implemented") } + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + @objc private func cancelPressed() { self.dismiss() } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index b567a0a2a8..6d5338baa8 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2794,6 +2794,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private weak var storyCameraTooltip: TooltipScreen? fileprivate func openStoryCamera(fromList: Bool) { + guard !self.context.isFrozen else { + let controller = self.context.sharedContext.makeAccountFreezeInfoScreen(context: self.context) + self.push(controller) + return + } + var reachedCountLimit = false var premiumNeeded = false var hasActiveCall = false @@ -4641,6 +4647,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } @objc fileprivate func composePressed() { + guard !self.context.isFrozen else { + let controller = self.context.sharedContext.makeAccountFreezeInfoScreen(context: self.context) + self.push(controller) + return + } + guard let navigationController = self.navigationController as? NavigationController else { return } diff --git a/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift b/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift index aa65028e56..f6630c9ac9 100644 --- a/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift +++ b/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift @@ -9,6 +9,7 @@ public final class SolidRoundedButtonComponent: Component { public typealias Theme = SolidRoundedButtonTheme public let title: String? + public let subtitle: String? public let label: String? public let badge: String? public let icon: UIImage? @@ -28,6 +29,7 @@ public final class SolidRoundedButtonComponent: Component { public init( title: String? = nil, + subtitle: String? = nil, label: String? = nil, badge: String? = nil, icon: UIImage? = nil, @@ -46,6 +48,7 @@ public final class SolidRoundedButtonComponent: Component { action: @escaping () -> Void ) { self.title = title + self.subtitle = subtitle self.label = label self.badge = badge self.icon = icon @@ -68,6 +71,9 @@ public final class SolidRoundedButtonComponent: Component { if lhs.title != rhs.title { return false } + if lhs.subtitle != rhs.subtitle { + return false + } if lhs.label != rhs.label { return false } @@ -147,6 +153,7 @@ public final class SolidRoundedButtonComponent: Component { if let button = self.button { button.title = component.title + button.subtitle = component.subtitle button.label = component.label button.badge = component.badge button.iconPosition = component.iconPosition diff --git a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift index e3b6141e82..4eeac4b0c1 100644 --- a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift +++ b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift @@ -242,6 +242,7 @@ open class ViewControllerComponentContainer: ViewController { public private(set) var validLayout: ContainerViewLayout? public var wasDismissed: (() -> Void)? + public var customProceed: (() -> Void)? public init( context: AccountContext, diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h index efdee05775..ba44c28837 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h @@ -67,9 +67,7 @@ - (SSignal *)coverImageSignalForItem:(NSObject *)item; - (void)setCoverImage:(UIImage *)image position:(NSNumber *)position forItem:(id)item; - (UIImage *)coverImageForItem:(NSObject *)item; - - (NSNumber *)coverPositionForItem:(NSObject *)item; -- (void)setCoverImage:(UIImage *)image position:(NSNumber *)position forItem:(id)item; - (void)setTemporaryRep:(id)rep forItem:(id)item; diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 61175ca87b..76082e2966 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -309,6 +309,12 @@ public enum PremiumSource: Equatable { } else { return false } + case let .auth(lhsPrice): + if case let .auth(rhsPrice) = rhs, lhsPrice == rhsPrice { + return true + } else { + return false + } } } @@ -357,6 +363,7 @@ public enum PremiumSource: Equatable { case folderTags case messageEffects case paidMessages + case auth(String) var identifier: String? { switch self { @@ -452,6 +459,8 @@ public enum PremiumSource: Equatable { return "effects" case .paidMessages: return "paid_messages" + case .auth: + return "auth" } } } @@ -1214,6 +1223,7 @@ final class PerkComponent: CombinedComponent { let subtitleColor: UIColor let arrowColor: UIColor let accentColor: UIColor + let displayArrow: Bool let badge: String? init( @@ -1225,6 +1235,7 @@ final class PerkComponent: CombinedComponent { subtitleColor: UIColor, arrowColor: UIColor, accentColor: UIColor, + displayArrow: Bool = true, badge: String? = nil ) { self.iconName = iconName @@ -1235,6 +1246,7 @@ final class PerkComponent: CombinedComponent { self.subtitleColor = subtitleColor self.arrowColor = arrowColor self.accentColor = accentColor + self.displayArrow = displayArrow self.badge = badge } @@ -1263,6 +1275,9 @@ final class PerkComponent: CombinedComponent { if lhs.accentColor != rhs.accentColor { return false } + if lhs.displayArrow != rhs.displayArrow { + return false + } if lhs.badge != rhs.badge { return false } @@ -1305,16 +1320,7 @@ final class PerkComponent: CombinedComponent { availableSize: iconSize, transition: context.transition ) - - let arrow = arrow.update( - component: BundleIconComponent( - name: "Item List/DisclosureArrow", - tintColor: component.arrowColor - ), - availableSize: context.availableSize, - transition: context.transition - ) - + let title = title.update( component: MultilineTextComponent( text: .plain( @@ -1391,9 +1397,20 @@ final class PerkComponent: CombinedComponent { ) let size = CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + spacing + subtitle.size.height + textBottomInset) - context.add(arrow - .position(CGPoint(x: context.availableSize.width - 7.0 - arrow.size.width / 2.0, y: size.height / 2.0)) - ) + + if component.displayArrow { + let arrow = arrow.update( + component: BundleIconComponent( + name: "Item List/DisclosureArrow", + tintColor: component.arrowColor + ), + availableSize: context.availableSize, + transition: context.transition + ) + context.add(arrow + .position(CGPoint(x: context.availableSize.width - 7.0 - arrow.size.width / 2.0, y: size.height / 2.0)) + ) + } return size } @@ -2086,6 +2103,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { foregroundColor: .white, iconName: perk.iconName ))), false), + accessory: accountContext != nil ? .arrow : nil, action: { [weak state] _ in guard let accountContext else { return @@ -2162,7 +2180,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { updateIsFocused(true) addAppLogEvent(postbox: accountContext.account.postbox, type: "premium.promo_screen_tap", data: ["item": perk.identifier]) - } + }, + highlighting: accountContext != nil ? .default : .disabled )))) i += 1 } @@ -3660,7 +3679,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent { if !buttonIsHidden { let buttonTitle: String - if isUnusedGift { + var buttonSubtitle: String? + if case let .auth(price) = context.component.source { + buttonTitle = "Sign up for \(price)" + buttonSubtitle = "Get Telegram Premium for 1 week" + } else if isUnusedGift { buttonTitle = environment.strings.Premium_Gift_ApplyLink } else if state.isPremium == true && state.canUpgrade { buttonTitle = state.isAnnual ? environment.strings.Premium_UpgradeForAnnual(state.price ?? "—").string : environment.strings.Premium_UpgradeFor(state.price ?? "—").string @@ -3668,10 +3691,12 @@ private final class PremiumIntroScreenComponent: CombinedComponent { buttonTitle = state.isAnnual ? environment.strings.Premium_SubscribeForAnnual(state.price ?? "—").string : environment.strings.Premium_SubscribeFor(state.price ?? "—").string } + let controller = environment.controller let sideInset: CGFloat = 16.0 let button = button.update( component: SolidRoundedButtonComponent( title: buttonTitle, + subtitle: buttonSubtitle, theme: SolidRoundedButtonComponent.Theme( backgroundColor: UIColor(rgb: 0x8878ff), backgroundColors: [ @@ -3687,7 +3712,12 @@ private final class PremiumIntroScreenComponent: CombinedComponent { gloss: true, isLoading: state.inProgress, action: { - state.buy() + if let controller = controller() as? PremiumIntroScreen, let customProceed = controller.customProceed { + controller.dismiss() + customProceed() + } else { + state.buy() + } } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: 50.0), diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift index e18a0ac730..7b724ca498 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift @@ -315,7 +315,7 @@ private func selectivePrivacyPeersControllerEntries(presentationData: Presentati entries.append(.footerItem(footer)) } - if !peers.isEmpty { + if !peers.isEmpty || state.enableForPremium || state.enableForBots { entries.append(.deleteItem(presentationData.strings.Privacy_Exceptions_DeleteAllExceptions)) } diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index 5574b7f9f1..3c10b7dca4 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -1412,7 +1412,7 @@ public final class SolidRoundedButtonView: UIView { } self.titleNode.attributedText = titleText - self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: theme.foregroundColor) + self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.medium(11.0), textColor: theme.foregroundColor.withAlphaComponent(0.7)) self.iconNode.image = generateTintedImage(image: self.iconNode.image, color: theme.foregroundColor) @@ -1472,7 +1472,7 @@ public final class SolidRoundedButtonView: UIView { } let titleSize = self.titleNode.updateLayout(buttonSize) - let spacingOffset: CGFloat = 9.0 + let spacingOffset: CGFloat = 7.0 let verticalInset: CGFloat = self.subtitle == nil ? floor((buttonFrame.height - titleSize.height) / 2.0) : floor((buttonFrame.height - titleSize.height) / 2.0) - spacingOffset let iconSpacing: CGFloat = self.iconSpacing let badgeSpacing: CGFloat = 6.0 @@ -1533,11 +1533,11 @@ public final class SolidRoundedButtonView: UIView { } if self.subtitle != self.subtitleNode.attributedText?.string { - self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: self.theme.foregroundColor) + self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.medium(11.0), textColor: self.theme.foregroundColor.withAlphaComponent(0.7)) } let subtitleSize = self.subtitleNode.updateLayout(buttonSize) - let subtitleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - subtitleSize.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - titleSize.height) / 2.0) + spacingOffset + 2.0), size: subtitleSize) + let subtitleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - subtitleSize.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - titleSize.height) / 2.0) + spacingOffset + 7.0), size: subtitleSize) transition.updateFrame(view: self.subtitleNode, frame: subtitleFrame) if previousSubtitle == nil && self.subtitle != nil { diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 4d7c541c9a..0efc2bec9d 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -577,6 +577,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1281329567] = { return Api.MessageAction.parse_messageActionGroupCallScheduled($0) } dict[-1615153660] = { return Api.MessageAction.parse_messageActionHistoryClear($0) } dict[1345295095] = { return Api.MessageAction.parse_messageActionInviteToGroupCall($0) } + dict[-1126755303] = { return Api.MessageAction.parse_messageActionPaidMessagesPrice($0) } + dict[-1407246387] = { return Api.MessageAction.parse_messageActionPaidMessagesRefunded($0) } dict[1102307842] = { return Api.MessageAction.parse_messageActionPaymentRefunded($0) } dict[-970673810] = { return Api.MessageAction.parse_messageActionPaymentSent($0) } dict[-6288180] = { return Api.MessageAction.parse_messageActionPaymentSentMe($0) } diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index 72842970c5..63df242808 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -363,6 +363,8 @@ public extension Api { case messageActionGroupCallScheduled(call: Api.InputGroupCall, scheduleDate: Int32) case messageActionHistoryClear case messageActionInviteToGroupCall(call: Api.InputGroupCall, users: [Int64]) + case messageActionPaidMessagesPrice(stars: Int64) + case messageActionPaidMessagesRefunded(count: Int32, stars: Int64) case messageActionPaymentRefunded(flags: Int32, peer: Api.Peer, currency: String, totalAmount: Int64, payload: Buffer?, charge: Api.PaymentCharge) case messageActionPaymentSent(flags: Int32, currency: String, totalAmount: Int64, invoiceSlug: String?, subscriptionUntilDate: Int32?) case messageActionPaymentSentMe(flags: Int32, currency: String, totalAmount: Int64, payload: Buffer, info: Api.PaymentRequestedInfo?, shippingOptionId: String?, charge: Api.PaymentCharge, subscriptionUntilDate: Int32?) @@ -595,6 +597,19 @@ public extension Api { serializeInt64(item, buffer: buffer, boxed: false) } break + case .messageActionPaidMessagesPrice(let stars): + if boxed { + buffer.appendInt32(-1126755303) + } + serializeInt64(stars, buffer: buffer, boxed: false) + break + case .messageActionPaidMessagesRefunded(let count, let stars): + if boxed { + buffer.appendInt32(-1407246387) + } + serializeInt32(count, buffer: buffer, boxed: false) + serializeInt64(stars, buffer: buffer, boxed: false) + break case .messageActionPaymentRefunded(let flags, let peer, let currency, let totalAmount, let payload, let charge): if boxed { buffer.appendInt32(1102307842) @@ -847,6 +862,10 @@ public extension Api { return ("messageActionHistoryClear", []) case .messageActionInviteToGroupCall(let call, let users): return ("messageActionInviteToGroupCall", [("call", call as Any), ("users", users as Any)]) + case .messageActionPaidMessagesPrice(let stars): + return ("messageActionPaidMessagesPrice", [("stars", stars as Any)]) + case .messageActionPaidMessagesRefunded(let count, let stars): + return ("messageActionPaidMessagesRefunded", [("count", count as Any), ("stars", stars as Any)]) case .messageActionPaymentRefunded(let flags, let peer, let currency, let totalAmount, let payload, let charge): return ("messageActionPaymentRefunded", [("flags", flags as Any), ("peer", peer as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("payload", payload as Any), ("charge", charge as Any)]) case .messageActionPaymentSent(let flags, let currency, let totalAmount, let invoiceSlug, let subscriptionUntilDate): @@ -1277,6 +1296,31 @@ public extension Api { return nil } } + public static func parse_messageActionPaidMessagesPrice(_ reader: BufferReader) -> MessageAction? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return Api.MessageAction.messageActionPaidMessagesPrice(stars: _1!) + } + else { + return nil + } + } + public static func parse_messageActionPaidMessagesRefunded(_ reader: BufferReader) -> MessageAction? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.MessageAction.messageActionPaidMessagesRefunded(count: _1!, stars: _2!) + } + else { + return nil + } + } public static func parse_messageActionPaymentRefunded(_ reader: BufferReader) -> MessageAction? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 03276a38be..d3fe483a2e 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -227,7 +227,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { } switch action { - case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionGiftStars, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe, .messageActionStarGift, .messageActionStarGiftUnique: + case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionGiftStars, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe, .messageActionStarGift, .messageActionStarGiftUnique, .messageActionPaidMessagesRefunded, .messageActionPaidMessagesPrice: break case let .messageActionChannelMigrateFrom(_, chatId): result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId))) diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index e4e7923481..2dccae65da 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -191,6 +191,10 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return nil } return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId)) + case let .messageActionPaidMessagesRefunded(count, stars): + return TelegramMediaAction(action: .paidMessagesRefunded(count: count, stars: stars)) + case let .messageActionPaidMessagesPrice(stars): + return TelegramMediaAction(action: .paidMessagesPriceEdited(stars: stars)) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 6dc1fdecb2..513375d750 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -132,6 +132,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case prizeStars(amount: Int64, isUnclaimed: Bool, boostPeerId: PeerId?, transactionId: String?, giveawayMessageId: MessageId?) case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, upgradeMessageId: Int32?, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?) case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?) + case paidMessagesRefunded(count: Int32, stars: Int64) + case paidMessagesPriceEdited(stars: Int64) public init(decoder: PostboxDecoder) { let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) @@ -256,6 +258,10 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), upgradeMessageId: decoder.decodeOptionalInt32ForKey("upgradeMessageId"), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId")) case 45: self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId")) + case 46: + self = .paidMessagesRefunded(count: decoder.decodeInt32ForKey("count", orElse: 0), stars: decoder.decodeInt64ForKey("stars", orElse: 0)) + case 47: + self = .paidMessagesPriceEdited(stars: decoder.decodeInt64ForKey("stars", orElse: 0)) default: self = .unknown } @@ -626,6 +632,13 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "savedId") } + case let .paidMessagesRefunded(count, stars): + encoder.encodeInt32(46, forKey: "_rawValue") + encoder.encodeInt32(count, forKey: "count") + encoder.encodeInt64(stars, forKey: "stars") + case let .paidMessagesPriceEdited(stars): + encoder.encodeInt32(47, forKey: "_rawValue") + encoder.encodeInt64(stars, forKey: "stars") } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift index b9630127c2..01422837b6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift @@ -109,7 +109,14 @@ func _internal_peerSendAsAvailablePeers(accountPeerId: PeerId, network: Network, return .single([]) } - if let channel = peer as? TelegramChannel, case .group = channel.info { + if let channel = peer as? TelegramChannel { + if case .group = channel.info { + + } else if channel.adminRights != nil || channel.flags.contains(.isCreator) { + + } else { + return .single([]) + } } else { return .single([]) } diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift index 5143dcad64..69c7260435 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift @@ -170,6 +170,12 @@ public final class PrincipalThemeEssentialGraphics { public let outgoingDateAndStatusRepliesIcon: UIImage public let mediaRepliesIcon: UIImage public let freeRepliesIcon: UIImage + + public let incomingDateAndStatusStarsIcon: UIImage + public let outgoingDateAndStatusStarsIcon: UIImage + public let mediaStarsIcon: UIImage + public let freeStarsIcon: UIImage + public let incomingDateAndStatusPinnedIcon: UIImage public let outgoingDateAndStatusPinnedIcon: UIImage public let mediaPinnedIcon: UIImage @@ -358,6 +364,12 @@ public final class PrincipalThemeEssentialGraphics { self.mediaRepliesIcon = generateTintedImage(image: repliesImage, color: .white)! self.freeRepliesIcon = generateTintedImage(image: repliesImage, color: serviceColor.primaryText)! + let starsImage = UIImage(bundleImageName: "Chat/Message/StarsCount")! + self.incomingDateAndStatusStarsIcon = generateTintedImage(image: starsImage, color: theme.message.incoming.secondaryTextColor)! + self.outgoingDateAndStatusStarsIcon = generateTintedImage(image: starsImage, color: theme.message.outgoing.secondaryTextColor)! + self.mediaStarsIcon = generateTintedImage(image: starsImage, color: .white)! + self.freeStarsIcon = generateTintedImage(image: starsImage, color: serviceColor.primaryText)! + let pinnedImage = UIImage(bundleImageName: "Chat/Message/Pinned")! self.incomingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.incoming.secondaryTextColor)! self.outgoingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.outgoing.secondaryTextColor)! @@ -479,6 +491,12 @@ public final class PrincipalThemeEssentialGraphics { self.mediaRepliesIcon = generateTintedImage(image: repliesImage, color: .white)! self.freeRepliesIcon = generateTintedImage(image: repliesImage, color: serviceColor.primaryText)! + let starsImage = UIImage(bundleImageName: "Chat/Message/StarsCount")! + self.incomingDateAndStatusStarsIcon = generateTintedImage(image: starsImage, color: theme.message.incoming.secondaryTextColor)! + self.outgoingDateAndStatusStarsIcon = generateTintedImage(image: starsImage, color: theme.message.outgoing.secondaryTextColor)! + self.mediaStarsIcon = generateTintedImage(image: starsImage, color: .white)! + self.freeStarsIcon = generateTintedImage(image: starsImage, color: serviceColor.primaryText)! + let pinnedImage = UIImage(bundleImageName: "Chat/Message/Pinned")! self.incomingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.incoming.secondaryTextColor)! self.outgoingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.outgoing.secondaryTextColor)! diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 89a13d50e5..4f75317468 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1196,6 +1196,33 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } } } + case let .paidMessagesRefunded(_, stars): + let starsString = strings.Notification_PaidMessageRefund_Stars(Int32(stars)) + if message.author?.id == accountPeerId, let messagePeer = message.peers[message.id.peerId] { + let peerName = EnginePeer(messagePeer).compactDisplayTitle + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(1, messagePeer.id)]) + attributes[0] = boldAttributes + let resultString = strings.Notification_PaidMessageRefundYou(starsString, peerName) + attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: attributes) + } else { + let peerName = message.author?.compactDisplayTitle ?? "" + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)]) + attributes[1] = boldAttributes + let resultString = strings.Notification_PaidMessageRefund(peerName, starsString) + attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: attributes) + } + case let .paidMessagesPriceEdited(stars): + let starsString = strings.Notification_PaidMessagePriceChanged_Stars(Int32(stars)) + if message.author?.id == accountPeerId { + let resultString = strings.Notification_PaidMessagePriceChangedYou(starsString) + attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + } else { + let peerName = message.author?.compactDisplayTitle ?? "" + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)]) + attributes[1] = boldAttributes + let resultString = strings.Notification_PaidMessagePriceChanged(peerName, starsString) + attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: attributes) + } case .unknown: attributedString = nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift index eca913ad51..5d7a6a9ae0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift @@ -413,7 +413,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { repliesImage = graphics.incomingDateAndStatusPinnedIcon } if (arguments.starsCount ?? 0) != 0 { - starsImage = graphics.incomingDateAndStatusRepliesIcon + starsImage = graphics.incomingDateAndStatusStarsIcon } case let .BubbleOutgoing(status): dateColor = arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor @@ -432,7 +432,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { repliesImage = graphics.outgoingDateAndStatusPinnedIcon } if (arguments.starsCount ?? 0) != 0 { - starsImage = graphics.outgoingDateAndStatusRepliesIcon + starsImage = graphics.outgoingDateAndStatusStarsIcon } case .ImageIncoming: dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor @@ -451,7 +451,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { repliesImage = graphics.mediaPinnedIcon } if (arguments.starsCount ?? 0) != 0 { - starsImage = graphics.mediaRepliesIcon + starsImage = graphics.mediaStarsIcon } case let .ImageOutgoing(status): dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor @@ -471,7 +471,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { repliesImage = graphics.mediaPinnedIcon } if (arguments.starsCount ?? 0) != 0 { - starsImage = graphics.mediaRepliesIcon + starsImage = graphics.mediaStarsIcon } case .FreeIncoming: let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper) @@ -492,7 +492,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { repliesImage = graphics.freePinnedIcon } if (arguments.starsCount ?? 0) != 0 { - starsImage = graphics.freeRepliesIcon + starsImage = graphics.freeStarsIcon } case let .FreeOutgoing(status): let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper) @@ -513,7 +513,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { repliesImage = graphics.freePinnedIcon } if (arguments.starsCount ?? 0) != 0 { - starsImage = graphics.freeRepliesIcon + starsImage = graphics.freeStarsIcon } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift index baf1702c7e..74e3de47ae 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift @@ -516,7 +516,7 @@ public final class PeerInfoCoverComponent: Component { let baseItemDistance: CGFloat = baseDistance + CGFloat(row) * baseRowDistance let itemDistanceFraction = max(0.0, min(1.0, baseItemDistance / (baseDistance * 2.0))) - let itemScaleFraction = patternScaleValueAt(fraction: component.avatarTransitionFraction, t: itemDistanceFraction, reverse: false) + let itemScaleFraction = patternScaleValueAt(fraction: min(1.0, component.avatarTransitionFraction * 1.56), t: itemDistanceFraction, reverse: false) let itemDistance = baseItemDistance * (1.0 - itemScaleFraction) + 20.0 * itemScaleFraction var itemAngle: CGFloat diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift index 6c81f40c57..776c0ea55e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift @@ -19,6 +19,7 @@ public final class PeerInfoGiftsCoverComponent: Component { public let giftsContext: ProfileGiftsContext public let hasBackground: Bool public let avatarCenter: CGPoint + public let avatarSize: CGSize public let defaultHeight: CGFloat public let avatarTransitionFraction: CGFloat public let statusBarHeight: CGFloat @@ -34,6 +35,7 @@ public final class PeerInfoGiftsCoverComponent: Component { giftsContext: ProfileGiftsContext, hasBackground: Bool, avatarCenter: CGPoint, + avatarSize: CGSize, defaultHeight: CGFloat, avatarTransitionFraction: CGFloat, statusBarHeight: CGFloat, @@ -48,6 +50,7 @@ public final class PeerInfoGiftsCoverComponent: Component { self.giftsContext = giftsContext self.hasBackground = hasBackground self.avatarCenter = avatarCenter + self.avatarSize = avatarSize self.defaultHeight = defaultHeight self.avatarTransitionFraction = avatarTransitionFraction self.statusBarHeight = statusBarHeight @@ -71,6 +74,9 @@ public final class PeerInfoGiftsCoverComponent: Component { if lhs.avatarCenter != rhs.avatarCenter { return false } + if lhs.avatarSize != rhs.avatarSize { + return false + } if lhs.defaultHeight != rhs.defaultHeight { return false } @@ -298,26 +304,25 @@ public final class PeerInfoGiftsCoverComponent: Component { } iconLayer.glowing = component.hasBackground - let centerPosition = component.avatarCenter - let finalPosition = iconPosition.center.offsetBy(dx: component.avatarCenter.x, dy: component.avatarCenter.y) - let itemScaleFraction = patternScaleValueAt(fraction: component.avatarTransitionFraction, t: 0.0, reverse: false) + let itemDistanceFraction = max(0.0, min(1.0, iconPosition.distance / 100.0)) + let itemScaleFraction = patternScaleValueAt(fraction: min(1.0, component.avatarTransitionFraction * 1.56), t: itemDistanceFraction, reverse: false) - func interpolateRect(from: CGPoint, to: CGPoint, t: CGFloat) -> CGPoint { + func interpolatePosition(from: PositionGenerator.Position, to: PositionGenerator.Position, t: CGFloat) -> PositionGenerator.Position { let clampedT = max(0, min(1, t)) - let interpolatedX = from.x + (to.x - from.x) * clampedT - let interpolatedY = from.y + (to.y - from.y) * clampedT + let interpolatedDistance = from.distance + (to.distance - from.distance) * clampedT + let interpolatedAngle = from.angle + (to.angle - from.angle) * clampedT - return CGPoint( - x: interpolatedX, - y: interpolatedY - ) + return PositionGenerator.Position(distance: interpolatedDistance, angle: interpolatedAngle, scale: from.scale) } - let effectivePosition = interpolateRect(from: finalPosition, to: centerPosition, t: itemScaleFraction) + let centerPosition = PositionGenerator.Position(distance: 0.0, angle: iconPosition.angle + .pi * 0.18, scale: iconPosition.scale) + let effectivePosition = interpolatePosition(from: iconPosition, to: centerPosition, t: itemScaleFraction) + let position = getAbsolutePosition(position: effectivePosition, centerPoint: component.avatarCenter) + iconTransition.setBounds(layer: iconLayer, bounds: CGRect(origin: .zero, size: iconSize)) - iconTransition.setPosition(layer: iconLayer, position: effectivePosition) + iconTransition.setPosition(layer: iconLayer, position: position) iconTransition.setScale(layer: iconLayer, scale: iconPosition.scale * (1.0 - itemScaleFraction)) iconTransition.setAlpha(layer: iconLayer, alpha: 1.0 - itemScaleFraction) @@ -638,8 +643,16 @@ private class GiftIconLayer: SimpleLayer { private struct PositionGenerator { struct Position { - let center: CGPoint + let distance: CGFloat + let angle: CGFloat let scale: CGFloat + + var relativeCartesian: CGPoint { + return CGPoint( + x: self.distance * cos(self.angle), + y: self.distance * sin(self.angle) + ) + } } let containerSize: CGSize @@ -700,15 +713,14 @@ private struct PositionGenerator { let orbitRangeSize = self.innerOrbitRange.max - self.innerOrbitRange.min let orbitDistanceFactor = self.innerOrbitRange.min + orbitRangeSize * CGFloat(self.lokiRng.next()) - let orbitDistance = orbitDistanceFactor * centerRadius + let distance = orbitDistanceFactor * centerRadius let angleRange: CGFloat = placeOnLeftSide ? .pi : .pi let angleOffset: CGFloat = placeOnLeftSide ? .pi/2 : -(.pi/2) let angle = angleOffset + angleRange * CGFloat(self.lokiRng.next()) - let absoluteX = centerPoint.x + orbitDistance * cos(angle) - let absoluteY = centerPoint.y + orbitDistance * sin(angle) - let absolutePosition = CGPoint(x: absoluteX, y: absoluteY) + // Get the absolute position to check boundaries and collisions + let absolutePosition = getAbsolutePosition(distance: distance, angle: angle, centerPoint: centerPoint) if absolutePosition.x - itemSize.width/2 < self.edgePadding || absolutePosition.x + itemSize.width/2 > self.containerSize.width - self.edgePadding || @@ -717,11 +729,6 @@ private struct PositionGenerator { continue } - let relativePosition = CGPoint( - x: absolutePosition.x - centerPoint.x, - y: absolutePosition.y - centerPoint.y - ) - let itemRect = CGRect( x: absolutePosition.x - itemSize.width/2, y: absolutePosition.y - itemSize.height/2, @@ -729,10 +736,12 @@ private struct PositionGenerator { height: itemSize.height ) - if self.isValidPosition(itemRect, existingPositions: positions.map { self.posToAbsolute($0.center, centerPoint: centerPoint) }, itemSize: itemSize) { + if self.isValidPosition(itemRect, existingPositions: positions.map { + getAbsolutePosition(distance: $0.distance, angle: $0.angle, centerPoint: centerPoint) + }, itemSize: itemSize) { let scaleRangeSize = max(self.scaleRange.min + 0.1, 0.75) - self.scaleRange.max let scale = self.scaleRange.max + scaleRangeSize * CGFloat(self.lokiRng.next()) - positions.append(Position(center: relativePosition, scale: scale)) + positions.append(Position(distance: distance, angle: angle, scale: scale)) if absolutePosition.x < centerPoint.x { leftPositions += 1 @@ -751,15 +760,14 @@ private struct PositionGenerator { let orbitRangeSize = self.outerOrbitRange.max - self.outerOrbitRange.min let orbitDistanceFactor = self.outerOrbitRange.min + orbitRangeSize * CGFloat(self.lokiRng.next()) - let orbitDistance = orbitDistanceFactor * centerRadius + let distance = orbitDistanceFactor * centerRadius let angleRange: CGFloat = placeOnLeftSide ? .pi : .pi let angleOffset: CGFloat = placeOnLeftSide ? .pi/2 : -(.pi/2) let angle = angleOffset + angleRange * CGFloat(self.lokiRng.next()) - let absoluteX = centerPoint.x + orbitDistance * cos(angle) - let absoluteY = centerPoint.y + orbitDistance * sin(angle) - let absolutePosition = CGPoint(x: absoluteX, y: absoluteY) + // Get the absolute position to check boundaries and collisions + let absolutePosition = getAbsolutePosition(distance: distance, angle: angle, centerPoint: centerPoint) if absolutePosition.x - itemSize.width/2 < self.edgePadding || absolutePosition.x + itemSize.width/2 > self.containerSize.width - self.edgePadding || @@ -768,11 +776,6 @@ private struct PositionGenerator { continue } - let relativePosition = CGPoint( - x: absolutePosition.x - centerPoint.x, - y: absolutePosition.y - centerPoint.y - ) - let itemRect = CGRect( x: absolutePosition.x - itemSize.width/2, y: absolutePosition.y - itemSize.height/2, @@ -780,12 +783,12 @@ private struct PositionGenerator { height: itemSize.height ) - if self.isValidPosition(itemRect, existingPositions: positions.map { self.posToAbsolute($0.center, centerPoint: centerPoint) }, itemSize: itemSize) { - let distance = hypot(absolutePosition.x - centerPoint.x, absolutePosition.y - centerPoint.y) - + if self.isValidPosition(itemRect, existingPositions: positions.map { + getAbsolutePosition(distance: $0.distance, angle: $0.angle, centerPoint: centerPoint) + }, itemSize: itemSize) { let normalizedDistance = min(distance / maxPossibleDistance, 1.0) let scale = self.scaleRange.max - normalizedDistance * (self.scaleRange.max - self.scaleRange.min) - positions.append(Position(center: relativePosition, scale: scale)) + positions.append(Position(distance: distance, angle: angle, scale: scale)) if absolutePosition.x < centerPoint.x { leftPositions += 1 @@ -798,8 +801,11 @@ private struct PositionGenerator { return positions } - private func posToAbsolute(_ relativePos: CGPoint, centerPoint: CGPoint) -> CGPoint { - return CGPoint(x: relativePos.x + centerPoint.x, y: relativePos.y + centerPoint.y) + func getAbsolutePosition(distance: CGFloat, angle: CGFloat, centerPoint: CGPoint) -> CGPoint { + return CGPoint( + x: centerPoint.x + distance * cos(angle), + y: centerPoint.y + distance * sin(angle) + ) } private func isValidPosition(_ rect: CGRect, existingPositions: [CGPoint], itemSize: CGSize) -> Bool { @@ -827,6 +833,20 @@ private struct PositionGenerator { } } +private func getAbsolutePosition(position: PositionGenerator.Position, centerPoint: CGPoint) -> CGPoint { + return CGPoint( + x: centerPoint.x + position.distance * cos(position.angle), + y: centerPoint.y + position.distance * sin(position.angle) + ) +} + +private func getAbsolutePosition(distance: CGFloat, angle: CGFloat, centerPoint: CGPoint) -> CGPoint { + return CGPoint( + x: centerPoint.x + distance * cos(angle), + y: centerPoint.y + distance * sin(angle) + ) +} + private func windowFunction(t: CGFloat) -> CGFloat { return bezierPoint(0.6, 0.0, 0.4, 1.0, t) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 1c0c8c45df..b0aedadc45 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -2352,6 +2352,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { giftsContext: profileGiftsContext, hasBackground: hasBackground, avatarCenter: apparentAvatarFrame.center, + avatarSize: apparentAvatarFrame.size, defaultHeight: backgroundDefaultHeight, avatarTransitionFraction: max(0.0, min(1.0, titleCollapseFraction + transitionFraction * 2.0)), statusBarHeight: statusBarHeight, diff --git a/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/Sources/AccountFreezeInfoScreen.swift b/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/Sources/AccountFreezeInfoScreen.swift index 64960bfc00..9de68c6b39 100644 --- a/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/Sources/AccountFreezeInfoScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/Sources/AccountFreezeInfoScreen.swift @@ -165,9 +165,9 @@ private final class SheetContent: CombinedComponent { iconName: "Account Freeze/Appeal", iconColor: linkColor, action: { - component.submitAppeal() - Queue.mainQueue().after(1.0) { - component.dismiss() + component.dismiss() + Queue.mainQueue().after(0.5) { + component.submitAppeal() } } )) @@ -201,9 +201,9 @@ private final class SheetContent: CombinedComponent { isEnabled: true, displaysProgress: false, action: { - component.submitAppeal() - Queue.mainQueue().after(1.0) { - component.dismiss() + component.dismiss() + Queue.mainQueue().after(0.5) { + component.submitAppeal() } } ), diff --git a/submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/Contents.json index 93a1282c31..ecd534d108 100644 --- a/submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "hand_30.pdf", + "filename" : "sandtimer_30.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/sandtimer_30.pdf b/submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/sandtimer_30.pdf similarity index 100% rename from submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/sandtimer_30.pdf rename to submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/sandtimer_30.pdf diff --git a/submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/Contents.json index ecd534d108..93a1282c31 100644 --- a/submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "sandtimer_30.pdf", + "filename" : "hand_30.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/hand_30.pdf b/submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/hand_30.pdf similarity index 100% rename from submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/hand_30.pdf rename to submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/hand_30.pdf diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/StarsCount.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/StarsCount.imageset/Contents.json new file mode 100644 index 0000000000..fa834c36d7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/StarsCount.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "msgstar.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/StarsCount.imageset/msgstar.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/StarsCount.imageset/msgstar.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5565014e2c1a06bad09ab7db408d9b2e8cdb0573 GIT binary patch literal 4264 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 map { appConfiguration in + return AccountFreezeConfiguration.with(appConfiguration: appConfiguration).freezeUntilDate != nil + } + |> distinctUntilChanged + |> deliverOnMainQueue).startStrict(next: { [weak self] isFrozen in + guard let self = self else { + return + } + self.isFrozen = isFrozen + }) } deinit { @@ -464,6 +480,7 @@ public final class AccountContextImpl: AccountContext { self.animatedEmojiStickersDisposable?.dispose() self.userLimitsConfigurationDisposable?.dispose() self.peerNameColorsConfigurationDisposable?.dispose() + self.isFrozenDisposable?.dispose() } public func storeSecureIdPassword(password: String) { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerFrozenAccount.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerFrozenAccount.swift new file mode 100644 index 0000000000..c4d0348db4 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerFrozenAccount.swift @@ -0,0 +1,32 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AsyncDisplayKit +import Display +import ContextUI +import UndoUI +import AccountContext +import ChatControllerInteraction +import AnimatedTextComponent +import ChatMessagePaymentAlertController +import TelegramPresentationData +import TelegramNotices + +extension ChatControllerImpl { + func presentAccountFrozenInfoIfNeeded() -> Bool { + if self.context.isFrozen { + let accountFreezeConfiguration = AccountFreezeConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) + if let freezeAppealUrl = accountFreezeConfiguration.freezeAppealUrl { + let components = freezeAppealUrl.components(separatedBy: "/") + if let username = components.last, let peer = self.presentationInterfaceState.renderedPeer?.peer, peer.addressName == username { + return false + } + } + self.push(self.context.sharedContext.makeAccountFreezeInfoScreen(context: self.context)) + return true + } + return false + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 20be263b74..b38259a3c5 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -1865,6 +1865,12 @@ extension ChatControllerImpl { guard let strongSelf = self, strongSelf.isNodeLoaded else { return } + + guard !strongSelf.presentAccountFrozenInfoIfNeeded() else { + completion(.immediate, {}) + return + } + if let messageId = messageId { let intrinsicCanSendMessagesHere = canSendMessagesToChat(strongSelf.presentationInterfaceState) var canSendMessagesHere = intrinsicCanSendMessagesHere @@ -2114,6 +2120,11 @@ extension ChatControllerImpl { }) }, deleteMessages: { [weak self] messages, contextController, completion in if let strongSelf = self, !messages.isEmpty { + guard !strongSelf.presentAccountFrozenInfoIfNeeded() else { + completion(.default) + return + } + let messageIds = Set(messages.map { $0.id }) strongSelf.messageContextDisposable.set((strongSelf.context.sharedContext.chatAvailableMessageActions(engine: strongSelf.context.engine, accountPeerId: strongSelf.context.account.peerId, messageIds: messageIds, keepUpdated: false) |> deliverOnMainQueue).startStrict(next: { actions in @@ -4416,13 +4427,10 @@ extension ChatControllerImpl { return } - let accountFreezeConfiguration = AccountFreezeConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) - if let _ = accountFreezeConfiguration.freezeUntilDate { - let controller = self.context.sharedContext.makeAccountFreezeInfoScreen(context: self.context) - self.push(controller) + guard !self.presentAccountFrozenInfoIfNeeded() else { return } - + guard let peerId = self.chatLocation.peerId, let cachedData = self.peerView?.cachedData as? CachedChannelData, let boostToUnrestrict = cachedData.boostsToUnrestrict else { return } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift index 66b8679ef6..785024337b 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -333,6 +333,9 @@ extension ChatControllerImpl { } controller?.dismissWithoutContent() + guard !self.presentAccountFrozenInfoIfNeeded() else { + return + } self.presentTagPremiumPaywall() } @@ -341,6 +344,11 @@ extension ChatControllerImpl { return } + guard !self.presentAccountFrozenInfoIfNeeded() else { + controller?.dismiss(completion: {}) + return + } + guard let message = messages.first else { return } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index f61918c4e7..957f6e77fe 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -3399,42 +3399,51 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } }, scheduleCurrentMessage: { [weak self] params in - if let strongSelf = self { - strongSelf.presentScheduleTimePicker(completion: { [weak self] time in - if let strongSelf = self { - if let _ = strongSelf.presentationInterfaceState.interfaceState.mediaDraftState { - strongSelf.sendMediaRecording(scheduleTime: time, messageEffect: (params?.effect).flatMap { - return ChatSendMessageEffect(id: $0.id) - }) - } else { - let silentPosting = strongSelf.presentationInterfaceState.interfaceState.silentPosting - strongSelf.chatDisplayNode.sendCurrentMessage(silentPosting: silentPosting, scheduleTime: time, messageEffect: (params?.effect).flatMap { - return ChatSendMessageEffect(id: $0.id) - }) { [weak self] in - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, saveInterfaceState: strongSelf.presentationInterfaceState.subject != .scheduledMessages, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } - }) - - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { - strongSelf.openScheduledMessages() - } + guard let self else { + return + } + guard !self.presentAccountFrozenInfoIfNeeded() else { + return + } + self.presentScheduleTimePicker(completion: { [weak self] time in + if let strongSelf = self { + if let _ = strongSelf.presentationInterfaceState.interfaceState.mediaDraftState { + strongSelf.sendMediaRecording(scheduleTime: time, messageEffect: (params?.effect).flatMap { + return ChatSendMessageEffect(id: $0.id) + }) + } else { + let silentPosting = strongSelf.presentationInterfaceState.interfaceState.silentPosting + strongSelf.chatDisplayNode.sendCurrentMessage(silentPosting: silentPosting, scheduleTime: time, messageEffect: (params?.effect).flatMap { + return ChatSendMessageEffect(id: $0.id) + }) { [weak self] in + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, saveInterfaceState: strongSelf.presentationInterfaceState.subject != .scheduledMessages, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } + }) + + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + strongSelf.openScheduledMessages() } } } } - }) - } - }, sendScheduledMessagesNow: { [weak self] messageIds in - if let strongSelf = self { - if let _ = strongSelf.presentationInterfaceState.slowmodeState { - if let rect = strongSelf.chatDisplayNode.frameForInputActionButton() { - strongSelf.interfaceInteraction?.displaySlowmodeTooltip(strongSelf.chatDisplayNode.view, rect) - } - return - } else { - let _ = strongSelf.context.engine.messages.sendScheduledMessageNowInteractively(messageId: messageIds.first!).startStandalone() } + }) + }, sendScheduledMessagesNow: { [weak self] messageIds in + guard let self else { + return + } + guard !self.presentAccountFrozenInfoIfNeeded() else { + return + } + + if let _ = self.presentationInterfaceState.slowmodeState { + if let rect = self.chatDisplayNode.frameForInputActionButton() { + self.interfaceInteraction?.displaySlowmodeTooltip(self.chatDisplayNode.view, rect) + } + return + } else { + let _ = self.context.engine.messages.sendScheduledMessageNowInteractively(messageId: messageIds.first!).startStandalone() } }, editScheduledMessagesTime: { [weak self] messageIds in if let strongSelf = self, let messageId = messageIds.first { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index 486f255b1c..f69d9ad7a9 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -21,15 +21,24 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState return (nil, nil) } - let accountFreezeConfiguration = AccountFreezeConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) - if let _ = accountFreezeConfiguration.freezeUntilDate { - if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { - return (currentPanel, nil) - } else { - let panel = ChatRestrictedInputPanelNode() - panel.context = context - panel.interfaceInteraction = interfaceInteraction - return (panel, nil) + if context.isFrozen { + var isActuallyFrozen = true + let accountFreezeConfiguration = AccountFreezeConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + if let freezeAppealUrl = accountFreezeConfiguration.freezeAppealUrl { + let components = freezeAppealUrl.components(separatedBy: "/") + if let username = components.last, let peer = chatPresentationInterfaceState.renderedPeer?.peer, peer.addressName == username { + isActuallyFrozen = false + } + } + if isActuallyFrozen { + if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { + return (currentPanel, nil) + } else { + let panel = ChatRestrictedInputPanelNode() + panel.context = context + panel.interfaceInteraction = interfaceInteraction + return (panel, nil) + } } } diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index bf2d24cdc6..f6b6715f03 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -644,7 +644,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return self.actionButtons.micButton } - private let startingBotDisposable = MetaDisposable() private let statusDisposable = MetaDisposable() override var interfaceInteraction: ChatPanelInterfaceInteraction? { didSet { @@ -655,28 +654,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self?.updateIsProcessingInlineRequest(value) }).strict()) } - if let startingBot = self.interfaceInteraction?.statuses?.startingBot { - self.startingBotDisposable.set((startingBot |> deliverOnMainQueue).startStrict(next: { [weak self] value in - if let strongSelf = self { - strongSelf.startingBotProgress = value - } - }).strict()) - } } } - - private var startingBotProgress = false { - didSet { -// if self.startingBotProgress != oldValue { -// if self.startingBotProgress { -// self.startButton.transitionToProgress() -// } else { -// self.startButton.transitionFromProgress() -// } -// } - } - } - + func updateInputTextState(_ state: ChatTextInputState, keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, accessoryItems: [ChatTextInputAccessoryItem], animated: Bool) { if let currentState = self.presentationInterfaceState { var updateAccessoryButtons = false @@ -1130,7 +1110,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch deinit { self.statusDisposable.dispose() - self.startingBotDisposable.dispose() self.tooltipController?.dismiss() self.currentEmojiSuggestion?.disposable.dispose() } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 65c74b7751..16a7303a7c 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2451,6 +2451,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSource = .animatedEmoji case .paidMessages: mappedSource = .paidMessages + case let .auth(price): + mappedSource = .auth(price) } return mappedSource } @@ -2465,13 +2467,13 @@ public final class SharedAccountContextImpl: SharedAccountContext { return controller } - public func makePremiumIntroController(sharedContext: SharedAccountContext, engine: TelegramEngineUnauthorized, inAppPurchaseManager: InAppPurchaseManager, source: PremiumIntroSource, dismissed: (() -> Void)?) -> ViewController { + public func makePremiumIntroController(sharedContext: SharedAccountContext, engine: TelegramEngineUnauthorized, inAppPurchaseManager: InAppPurchaseManager, source: PremiumIntroSource, proceed: (() -> Void)?) -> ViewController { var modal = true if case .settings = source { modal = false } let controller = PremiumIntroScreen(screenContext: .sharedContext(sharedContext, engine, inAppPurchaseManager), source: self.mapIntroSource(source: source), modal: modal) - controller.wasDismissed = dismissed + controller.customProceed = proceed return controller } From 5ee53ad99671a59a0e57720449819c54a819bb6e Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 12 Mar 2025 00:40:11 +0400 Subject: [PATCH 15/19] Various fixes --- .../Sources/ContactListNode.swift | 2 +- .../Sources/InAppPurchaseManager.swift | 10 +- .../LegacyComponents/TGMediaEditingContext.h | 2 - .../Sources/EmojiHeaderComponent.swift | 9 +- .../Sources/PremiumIntroScreen.swift | 2 + ...ectivePrivacySettingsPeersController.swift | 2 +- .../TelegramEngine/Messages/SendAsPeers.swift | 9 +- .../Payments/BotPaymentForm.swift | 2 +- .../TelegramEngine/Payments/Stars.swift | 26 ++++ .../PresentationThemeEssentialGraphics.swift | 18 +++ .../Sources/TonFormat.swift | 2 +- .../ChatEmptyNode/Sources/ChatEmptyNode.swift | 25 ++-- .../ChatMessageAnimatedStickerItemNode.swift | 4 + .../ChatMessageAttachedContentNode.swift | 4 + .../Sources/ChatMessageBubbleItemNode.swift | 12 +- .../ChatMessageContactBubbleContentNode.swift | 4 + .../ChatMessageDateAndStatusNode.swift | 122 +++++++++++++++++- ...hatMessageFactCheckBubbleContentNode.swift | 4 + ...ChatMessageGiveawayBubbleContentNode.swift | 1 + .../ChatMessageInteractiveFileNode.swift | 4 + ...atMessageInteractiveInstantVideoNode.swift | 4 + .../ChatMessageInteractiveMediaNode.swift | 4 + .../ChatMessageMapBubbleContentNode.swift | 4 + .../ChatMessageMediaBubbleContentNode.swift | 4 + .../ChatMessagePollBubbleContentNode.swift | 4 + ...atMessageRestrictedBubbleContentNode.swift | 4 + .../Sources/ChatMessageStickerItemNode.swift | 4 + .../ChatMessageTextBubbleContentNode.swift | 4 + .../Sources/ChatUserInfoItem.swift | 2 +- .../Sources/GiftOptionsScreen.swift | 7 +- .../Sources/GiftSetupScreen.swift | 12 +- .../Sources/GiftViewScreen.swift | 30 +++-- .../Sources/PeerInfoGiftsCoverComponent.swift | 10 +- ...oHeaderNavigationButtonContainerNode.swift | 2 +- .../Sources/PeerInfoHeaderNode.swift | 4 + .../Sources/PeerInfoGiftsPaneNode.swift | 9 +- .../Stars/StarsBalanceOverlayComponent/BUILD | 1 + .../StarsBalanceOverlayComponent.swift | 45 ++++++- .../Sources/StarsTransactionScreen.swift | 6 +- .../Sources/StarsTransferScreen.swift | 11 +- .../Message/StarsCount.imageset/Contents.json | 12 ++ .../Message/StarsCount.imageset/msgstar.pdf | Bin 0 -> 4264 bytes .../TelegramUI/Sources/AccountContext.swift | 3 +- .../Chat/ChatControllerOpenWebApp.swift | 17 ++- .../Chat/ChatControllerPaidMessage.swift | 5 +- .../ChatReportPeerTitlePanelNode.swift | 96 ++++++++++---- 46 files changed, 474 insertions(+), 94 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/StarsCount.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/StarsCount.imageset/msgstar.pdf diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index f33c2c5dba..4e29b6eb61 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -591,7 +591,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis allSelected = false } var actionTitle: String? - if peerIds.count > 1 { + if !"".isEmpty, peerIds.count > 1 { actionTitle = allSelected ? strings.Premium_Gift_ContactSelection_DeselectAll.uppercased() : strings.Premium_Gift_ContactSelection_SelectAll.uppercased() } let header: ListViewItemHeader? = ChatListSearchItemHeader(type: .text(title.uppercased(), AnyHashable(10 * sectionId + (allSelected ? 1 : 0))), theme: theme, strings: strings, actionTitle: actionTitle, action: { _ in diff --git a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift index 0c955d674c..a35fe3344c 100644 --- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift +++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift @@ -231,6 +231,8 @@ public final class InAppPurchaseManager: NSObject { private let disposableSet = DisposableDict() + private var lastRequestTimestamp: Double? + public init(engine: TelegramEngine) { self.engine = engine @@ -255,11 +257,15 @@ public final class InAppPurchaseManager: NSObject { productRequest.start() self.productRequest = productRequest + self.lastRequestTimestamp = CFAbsoluteTimeGetCurrent() } public var availableProducts: Signal<[Product], NoError> { - if self.products.isEmpty && self.productRequest == nil { - self.requestProducts() + if self.products.isEmpty { + if let lastRequestTimestamp, CFAbsoluteTimeGetCurrent() - lastRequestTimestamp > 10.0 { + Logger.shared.log("InAppPurchaseManager", "No available products, rerequest") + self.requestProducts() + } } return self.productsPromise.get() } diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h index efdee05775..ba44c28837 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h @@ -67,9 +67,7 @@ - (SSignal *)coverImageSignalForItem:(NSObject *)item; - (void)setCoverImage:(UIImage *)image position:(NSNumber *)position forItem:(id)item; - (UIImage *)coverImageForItem:(NSObject *)item; - - (NSNumber *)coverPositionForItem:(NSObject *)item; -- (void)setCoverImage:(UIImage *)image position:(NSNumber *)position forItem:(id)item; - (void)setTemporaryRep:(id)rep forItem:(id)item; diff --git a/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift b/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift index cba245039f..59dd08228d 100644 --- a/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift +++ b/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift @@ -64,6 +64,7 @@ class EmojiHeaderComponent: Component { } weak var animateFrom: UIView? + var sourceRect: CGRect? weak var containerView: UIView? let statusView: ComponentHostView @@ -116,8 +117,13 @@ class EmojiHeaderComponent: Component { let initialPosition = self.statusView.center let targetPosition = self.statusView.superview!.convert(self.statusView.center, to: containerView) - let sourcePosition = animateFrom.superview!.convert(animateFrom.center, to: containerView).offsetBy(dx: 0.0, dy: 0.0) + var sourceOffset: CGPoint = .zero + if let sourceRect = self.sourceRect { + sourceOffset = CGPoint(x: sourceRect.center.x - animateFrom.frame.width / 2.0, y: 0.0) + } + let sourcePosition = animateFrom.superview!.convert(animateFrom.center, to: containerView).offsetBy(dx: sourceOffset.x, dy: sourceOffset.y) + containerView.addSubview(self.statusView) self.statusView.center = targetPosition @@ -127,6 +133,7 @@ class EmojiHeaderComponent: Component { self.statusView.layer.animatePosition(from: sourcePosition, to: targetPosition, duration: 0.55, timingFunction: kCAMediaTimingFunctionSpring) Queue.mainQueue().after(0.55, { + self.statusView.layer.removeAllAnimations() self.addSubview(self.statusView) self.statusView.center = initialPosition }) diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 7d0d43370a..88e5de88f5 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -3706,6 +3706,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { } public weak var sourceView: UIView? + public var sourceRect: CGRect? public weak var containerView: UIView? public var animationColor: UIColor? @@ -3884,6 +3885,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { if let sourceView = self.sourceView { view.animateFrom = sourceView + view.sourceRect = self.sourceRect view.containerView = self.containerView view.animateIn() diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift index e18a0ac730..7b724ca498 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift @@ -315,7 +315,7 @@ private func selectivePrivacyPeersControllerEntries(presentationData: Presentati entries.append(.footerItem(footer)) } - if !peers.isEmpty { + if !peers.isEmpty || state.enableForPremium || state.enableForBots { entries.append(.deleteItem(presentationData.strings.Privacy_Exceptions_DeleteAllExceptions)) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift index b9630127c2..01422837b6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift @@ -109,7 +109,14 @@ func _internal_peerSendAsAvailablePeers(accountPeerId: PeerId, network: Network, return .single([]) } - if let channel = peer as? TelegramChannel, case .group = channel.info { + if let channel = peer as? TelegramChannel { + if case .group = channel.info { + + } else if channel.adminRights != nil || channel.flags.contains(.isCreator) { + + } else { + return .single([]) + } } else { return .single([]) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index b5c2e11a66..8817f48613 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -395,7 +395,7 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv var flags: Int32 = 0 var message: Api.TextWithEntities? if let text, !text.isEmpty { - flags |= (1 << 1) + flags |= (1 << 0) message = .textWithEntities(text: text, entities: entities.flatMap { apiEntitiesFromMessageTextEntities($0, associatedPeers: SimpleDictionary()) } ?? []) } return .inputInvoicePremiumGiftStars(flags: flags, userId: inputUser, months: option.months, message: message) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 2587b5fc2b..13c30a2bb6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -568,6 +568,21 @@ private final class StarsContextImpl { self._state = state self._statePromise.set(.single(state)) } + + var onUpdate: Signal { + return self._statePromise.get() + |> take(until: { value in + if let value { + if !value.flags.contains(.isPendingBalance) { + return SignalTakeAction(passthrough: true, complete: true) + } + } + return SignalTakeAction(passthrough: false, complete: false) + }) + |> map { _ in + return Void() + } + } } private extension StarsContext.State.Transaction { @@ -1011,6 +1026,17 @@ public final class StarsContext { } } + public var onUpdate: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.onUpdate.start(next: { value in + subscriber.putNext(value) + })) + } + return disposable + } + } init(account: Account) { self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift index 5143dcad64..a2c2a1dabb 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift @@ -170,6 +170,12 @@ public final class PrincipalThemeEssentialGraphics { public let outgoingDateAndStatusRepliesIcon: UIImage public let mediaRepliesIcon: UIImage public let freeRepliesIcon: UIImage + + public let incomingDateAndStatusStarsIcon: UIImage + public let outgoingDateAndStatusStarsIcon: UIImage + public let mediaStarsIcon: UIImage + public let freeStarsIcon: UIImage + public let incomingDateAndStatusPinnedIcon: UIImage public let outgoingDateAndStatusPinnedIcon: UIImage public let mediaPinnedIcon: UIImage @@ -358,6 +364,12 @@ public final class PrincipalThemeEssentialGraphics { self.mediaRepliesIcon = generateTintedImage(image: repliesImage, color: .white)! self.freeRepliesIcon = generateTintedImage(image: repliesImage, color: serviceColor.primaryText)! + let starsImage = UIImage(bundleImageName: "Chat/Message/StarsCount")! + self.incomingDateAndStatusStarsIcon = generateTintedImage(image: starsImage, color: theme.message.incoming.secondaryTextColor)! + self.outgoingDateAndStatusStarsIcon = generateTintedImage(image: starsImage, color: theme.message.outgoing.secondaryTextColor)! + self.mediaStarsIcon = generateTintedImage(image: starsImage, color: .white)! + self.freeStarsIcon = generateTintedImage(image: starsImage, color: serviceColor.primaryText)! + let pinnedImage = UIImage(bundleImageName: "Chat/Message/Pinned")! self.incomingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.incoming.secondaryTextColor)! self.outgoingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.outgoing.secondaryTextColor)! @@ -479,6 +491,12 @@ public final class PrincipalThemeEssentialGraphics { self.mediaRepliesIcon = generateTintedImage(image: repliesImage, color: .white)! self.freeRepliesIcon = generateTintedImage(image: repliesImage, color: serviceColor.primaryText)! + let starsImage = UIImage(bundleImageName: "Chat/Message/StarsCount")! + self.incomingDateAndStatusStarsIcon = generateTintedImage(image: starsImage, color: theme.message.incoming.secondaryTextColor)! + self.outgoingDateAndStatusStarsIcon = generateTintedImage(image: starsImage, color: theme.message.outgoing.secondaryTextColor)! + self.mediaStarsIcon = generateTintedImage(image: starsImage, color: .white)! + self.freeStarsIcon = generateTintedImage(image: starsImage, color: serviceColor.primaryText)! + let pinnedImage = UIImage(bundleImageName: "Chat/Message/Pinned")! self.incomingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.incoming.secondaryTextColor)! self.outgoingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.outgoing.secondaryTextColor)! diff --git a/submodules/TelegramStringFormatting/Sources/TonFormat.swift b/submodules/TelegramStringFormatting/Sources/TonFormat.swift index de64f93e82..c237580f34 100644 --- a/submodules/TelegramStringFormatting/Sources/TonFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/TonFormat.swift @@ -81,7 +81,7 @@ public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDate public func formatStarsAmountText(_ amount: StarsAmount, dateTimeFormat: PresentationDateTimeFormat, showPlus: Bool = false) -> String { var balanceText = presentationStringsFormattedNumber(Int32(amount.value), dateTimeFormat.groupingSeparator) - let fraction = Double(amount.nanos) / 10e6 + let fraction = abs(Double(amount.nanos)) / 10e6 if fraction > 0.0 { balanceText.append(dateTimeFormat.decimalSeparator) balanceText.append("\(Int32(fraction))") diff --git a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift index b63ae30563..1478bbb023 100644 --- a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift @@ -1213,9 +1213,9 @@ public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatE private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? - private let stars: StarsAmount? + private let stars: Int64? - public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?, stars: StarsAmount?) { + public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?, stars: Int64?) { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) self.isPremiumDisabled = premiumConfiguration.isPremiumDisabled self.stars = stars @@ -1295,7 +1295,7 @@ public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatE } ) if let amount = self.stars { - let starsString = presentationStringsFormattedNumber(Int32(amount.value), interfaceState.dateTimeFormat.groupingSeparator) + let starsString = presentationStringsFormattedNumber(Int32(amount), interfaceState.dateTimeFormat.groupingSeparator) let rawText: String if self.isPremiumDisabled { rawText = interfaceState.strings.Chat_EmptyStatePaidMessagingDisabled_Text(peerTitle, " $ \(starsString)").string @@ -1426,7 +1426,7 @@ private enum ChatEmptyNodeContentType: Equatable { case greeting case topic case premiumRequired - case starsRequired + case starsRequired(Int64) } private final class EmptyAttachedDescriptionNode: HighlightTrackingButtonNode { @@ -1815,8 +1815,8 @@ public final class ChatEmptyNode: ASDisplayNode { } else if let _ = interfaceState.peerNearbyData { contentType = .peerNearby } else if let peer = peer as? TelegramUser { - if let _ = interfaceState.sendPaidMessageStars { - contentType = .starsRequired + if let sendPaidMessageStars = interfaceState.sendPaidMessageStars, interfaceState.businessIntro == nil { + contentType = .starsRequired(sendPaidMessageStars.value) } else if interfaceState.isPremiumRequiredForMessaging { contentType = .premiumRequired } else { @@ -1881,8 +1881,8 @@ public final class ChatEmptyNode: ASDisplayNode { node = ChatEmptyNodeTopicChatContent(context: self.context) case .premiumRequired: node = ChatEmptyNodePremiumRequiredChatContent(context: self.context, interaction: self.interaction, stars: nil) - case .starsRequired: - node = ChatEmptyNodePremiumRequiredChatContent(context: self.context, interaction: self.interaction, stars: interfaceState.sendPaidMessageStars) + case let .starsRequired(stars): + node = ChatEmptyNodePremiumRequiredChatContent(context: self.context, interaction: self.interaction, stars: stars) } self.content = (contentType, node) self.addSubnode(node) @@ -1893,8 +1893,13 @@ public final class ChatEmptyNode: ASDisplayNode { node.layer.animateScale(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) } } - self.isUserInteractionEnabled = [.peerNearby, .greeting, .premiumRequired, .starsRequired, .cloud].contains(contentType) - + switch contentType { + case .peerNearby, .greeting, .premiumRequired, .starsRequired, .cloud: + self.isUserInteractionEnabled = true + default: + self.isUserInteractionEnabled = false + } + let displayRect = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom)) var contentSize = CGSize() diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 1064b9d35a..d967016c40 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1044,6 +1044,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var edited = false var viewCount: Int? = nil var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -1057,6 +1058,8 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -1086,6 +1089,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { areReactionsTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: messageEffect, replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.message), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 297d1a9980..2df108a4e8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -675,6 +675,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: context.account.peerId, accountPeer: associatedData.accountPeer, message: message) if message.isRestricted(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) || presentationData.isPreview { dateReactionsAndPeers = ([], []) @@ -688,6 +689,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -747,6 +750,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { areReactionsTags: message.areReactionsTags(accountPeerId: context.account.peerId), messageEffect: message.messageEffect(availableMessageEffects: associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: message), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index ffa7a26e05..6eb157593b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -130,7 +130,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ result.append((message, ChatMessageRestrictedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false break outer - } else if let _ = attribute as? PaidStarsMessageAttribute, !addedPriceInfo { + } else if let _ = attribute as? PaidStarsMessageAttribute, !addedPriceInfo, message.id.peerId.namespace == Namespaces.Peer.CloudUser { result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) addedPriceInfo = true } @@ -299,7 +299,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } if isMediaInverted { - result.insert((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default)), at: 0) + result.insert((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default)), at: addedPriceInfo ? 1 : 0) } else { result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default))) needReactions = false @@ -327,7 +327,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } if let attribute = message.attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute, attribute.leadingPreview { - result.insert((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)), at: 0) + result.insert((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)), at: addedPriceInfo ? 1 : 0) } else { result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } @@ -362,7 +362,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ if result.isEmpty { needReactions = false } - result.insert((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)), at: 0) + result.insert((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)), at: addedPriceInfo ? 1 : 0) } else { result.append((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false @@ -2276,6 +2276,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: message) if message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -2289,6 +2290,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -2337,6 +2340,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI areReactionsTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.message.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: message), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift index e6156462fc..a64006553e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift @@ -233,6 +233,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -246,6 +247,8 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -300,6 +303,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: messageEffect, replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift index 7665ed8d47..f77630f42f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift @@ -195,6 +195,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { var areReactionsTags: Bool var messageEffect: AvailableMessageEffects.MessageEffect? var replyCount: Int + var starsCount: Int64? var isPinned: Bool var hasAutoremove: Bool var canViewReactionList: Bool @@ -218,6 +219,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { areReactionsTags: Bool, messageEffect: AvailableMessageEffects.MessageEffect?, replyCount: Int, + starsCount: Int64?, isPinned: Bool, hasAutoremove: Bool, canViewReactionList: Bool, @@ -240,6 +242,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { self.areReactionsTags = areReactionsTags self.messageEffect = messageEffect self.replyCount = replyCount + self.starsCount = starsCount self.isPinned = isPinned self.hasAutoremove = hasAutoremove self.canViewReactionList = canViewReactionList @@ -262,7 +265,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { private var repliesIcon: ASImageNode? private var selfExpiringIcon: ASImageNode? private var replyCountNode: TextNode? - + private var starsIcon: ASImageNode? + private var starsCountNode: TextNode? + private var type: ChatMessageDateAndStatusType? private var theme: ChatPresentationThemeData? private var layoutSize: CGSize? @@ -316,12 +321,14 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { var currentBackgroundNode = self.backgroundNode var currentImpressionIcon = self.impressionIcon var currentRepliesIcon = self.repliesIcon - + var currentStarsIcon = self.starsIcon + let currentType = self.type let currentTheme = self.theme let makeReplyCountLayout = TextNode.asyncLayout(self.replyCountNode) - + let makeStarsCountLayout = TextNode.asyncLayout(self.starsCountNode) + let reactionButtonsContainer = self.reactionButtonsContainer return { [weak self] arguments in @@ -337,7 +344,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { let clockMinImage: UIImage? var impressionImage: UIImage? var repliesImage: UIImage? - + var starsImage: UIImage? + let themeUpdated = arguments.presentationData.theme != currentTheme || arguments.type != currentType let graphics = PresentationResourcesChat.principalGraphics(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, bubbleCorners: arguments.presentationData.chatBubbleCorners) @@ -404,6 +412,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } else if arguments.isPinned { repliesImage = graphics.incomingDateAndStatusPinnedIcon } + if (arguments.starsCount ?? 0) != 0 { + starsImage = graphics.incomingDateAndStatusStarsIcon + } case let .BubbleOutgoing(status): dateColor = arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor outgoingStatus = status @@ -420,6 +431,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } else if arguments.isPinned { repliesImage = graphics.outgoingDateAndStatusPinnedIcon } + if (arguments.starsCount ?? 0) != 0 { + starsImage = graphics.outgoingDateAndStatusStarsIcon + } case .ImageIncoming: dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor backgroundImage = graphics.dateAndStatusMediaBackground @@ -436,6 +450,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } else if arguments.isPinned { repliesImage = graphics.mediaPinnedIcon } + if (arguments.starsCount ?? 0) != 0 { + starsImage = graphics.mediaStarsIcon + } case let .ImageOutgoing(status): dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor outgoingStatus = status @@ -453,6 +470,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } else if arguments.isPinned { repliesImage = graphics.mediaPinnedIcon } + if (arguments.starsCount ?? 0) != 0 { + starsImage = graphics.mediaStarsIcon + } case .FreeIncoming: let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper) dateColor = serviceColor.primaryText @@ -471,6 +491,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } else if arguments.isPinned { repliesImage = graphics.freePinnedIcon } + if (arguments.starsCount ?? 0) != 0 { + starsImage = graphics.freeStarsIcon + } case let .FreeOutgoing(status): let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper) dateColor = serviceColor.primaryText @@ -489,6 +512,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } else if arguments.isPinned { repliesImage = graphics.freePinnedIcon } + if (arguments.starsCount ?? 0) != 0 { + starsImage = graphics.freeStarsIcon + } } var updatedDateText = arguments.dateText @@ -541,6 +567,20 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { currentRepliesIcon = nil } + var starsIconSize = CGSize() + if let starsImage = starsImage { + if currentStarsIcon == nil { + let iconNode = ASImageNode() + iconNode.isLayerBacked = true + iconNode.displayWithoutProcessing = true + iconNode.displaysAsynchronously = false + currentStarsIcon = iconNode + } + starsIconSize = starsImage.size + } else { + currentStarsIcon = nil + } + if let outgoingStatus = outgoingStatus { switch outgoingStatus { case .Sending: @@ -652,7 +692,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } var replyCountLayoutAndApply: (TextNodeLayout, () -> TextNode)? - + var starsCountLayoutAndApply: (TextNodeLayout, () -> TextNode)? + let reactionSize: CGFloat = 8.0 let reactionSpacing: CGFloat = 2.0 let reactionTrailingSpacing: CGFloat = 6.0 @@ -671,11 +712,29 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { let layoutAndApply = makeReplyCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0))) reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0 + if arguments.starsCount != nil { + reactionInset += 3.0 + } replyCountLayoutAndApply = layoutAndApply } else if arguments.isPinned { reactionInset += 12.0 } + if let starsCount = arguments.starsCount, starsCount > 0 { + let countString: String + if starsCount > 1000000 { + countString = "\(starsCount / 1000000)M" + } else if starsCount > 1000 { + countString = "\(starsCount / 1000)K" + } else { + countString = "\(starsCount)" + } + + let layoutAndApply = makeStarsCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0))) + reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0 + starsCountLayoutAndApply = layoutAndApply + } + if arguments.messageEffect != nil { reactionInset += 13.0 } @@ -1227,6 +1286,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { let replyCountFrame = CGRect(origin: CGPoint(x: reactionOffset + 4.0, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: layout.size) animation.animator.updateFrame(layer: node.layer, frame: replyCountFrame, completion: nil) reactionOffset += 4.0 + layout.size.width + if currentStarsIcon != nil { + reactionOffset += 8.0 + } } else if let replyCountNode = strongSelf.replyCountNode { strongSelf.replyCountNode = nil if animation.isAnimated { @@ -1237,6 +1299,56 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { replyCountNode.removeFromSupernode() } } + + if let currentStarsIcon = currentStarsIcon { + currentStarsIcon.displaysAsynchronously = false + if currentStarsIcon.image !== starsImage { + currentStarsIcon.image = starsImage + } + if currentStarsIcon.supernode == nil { + strongSelf.starsIcon = currentStarsIcon + strongSelf.addSubnode(currentStarsIcon) + if animation.isAnimated { + currentStarsIcon.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + } + let starsIconFrame = CGRect(origin: CGPoint(x: reactionOffset - 2.0, y: backgroundInsets.top + offset + verticalInset + floor((date.size.height - starsIconSize.height) / 2.0)), size: starsIconSize) + animation.animator.updateFrame(layer: currentStarsIcon.layer, frame: starsIconFrame, completion: nil) + reactionOffset += 9.0 + } else if let starsIcon = strongSelf.starsIcon { + strongSelf.starsIcon = nil + if animation.isAnimated { + starsIcon.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak starsIcon] _ in + starsIcon?.removeFromSupernode() + }) + } else { + starsIcon.removeFromSupernode() + } + } + + if let (layout, apply) = starsCountLayoutAndApply { + let node = apply() + if strongSelf.starsCountNode !== node { + strongSelf.starsCountNode?.removeFromSupernode() + strongSelf.addSubnode(node) + strongSelf.starsCountNode = node + if animation.isAnimated { + node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + } + let starsCountFrame = CGRect(origin: CGPoint(x: reactionOffset + 4.0, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: layout.size) + animation.animator.updateFrame(layer: node.layer, frame: starsCountFrame, completion: nil) + reactionOffset += 4.0 + layout.size.width + } else if let starsCountNode = strongSelf.starsCountNode { + strongSelf.starsCountNode = nil + if animation.isAnimated { + starsCountNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak starsCountNode] _ in + starsCountNode?.removeFromSupernode() + }) + } else { + starsCountNode.removeFromSupernode() + } + } } }) }) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift index a2e2b5deef..a41e7a8686 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift @@ -295,6 +295,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode var rawText = "" var rawEntities: [MessageTextEntity] = [] var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -311,6 +312,8 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -447,6 +450,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift index e939dc18e6..263522c344 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift @@ -578,6 +578,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode, areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: nil, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index 5357300699..88c6dd9b03 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -898,6 +898,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: arguments.context.account.peerId, accountPeer: arguments.associatedData.accountPeer, message: arguments.topMessage) if arguments.topMessage.isRestricted(platform: "ios", contentSettings: arguments.context.currentContentSettings.with { $0 }) || arguments.presentationData.isPreview { dateReactionsAndPeers = ([], []) @@ -911,6 +912,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { if let channel = arguments.message.peers[arguments.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, arguments.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } if arguments.forcedIsEdited { @@ -956,6 +959,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { areReactionsTags: arguments.message.areReactionsTags(accountPeerId: arguments.context.account.peerId), messageEffect: arguments.message.messageEffect(availableMessageEffects: arguments.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: arguments.isPinned && !arguments.associatedData.isInPinnedListMode, hasAutoremove: arguments.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: arguments.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index a55f886df6..f95da1047c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -524,6 +524,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { let sentViaBot = false var viewCount: Int? = nil var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -537,6 +538,8 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -583,6 +586,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: messageEffect, replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 5946804836..468ea63fdf 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -83,6 +83,7 @@ public struct ChatMessageDateAndStatus { public var dateReactions: [MessageReaction] public var dateReactionPeers: [(MessageReaction.Reaction, EnginePeer)] public var dateReplies: Int + public var starsCount: Int64? public var isPinned: Bool public var dateText: String @@ -93,6 +94,7 @@ public struct ChatMessageDateAndStatus { dateReactions: [MessageReaction], dateReactionPeers: [(MessageReaction.Reaction, EnginePeer)], dateReplies: Int, + starsCount: Int64?, isPinned: Bool, dateText: String ) { @@ -102,6 +104,7 @@ public struct ChatMessageDateAndStatus { self.dateReactions = dateReactions self.dateReactionPeers = dateReactionPeers self.dateReplies = dateReplies + self.starsCount = starsCount self.isPinned = isPinned self.dateText = dateText } @@ -1118,6 +1121,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr areReactionsTags: message.areReactionsTags(accountPeerId: context.account.peerId), messageEffect: messageEffect, replyCount: dateAndStatus.dateReplies, + starsCount: dateAndStatus.starsCount, isPinned: dateAndStatus.isPinned, hasAutoremove: message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: message), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift index 2c8c18860a..a9511b99c1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift @@ -192,6 +192,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -205,6 +206,8 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -284,6 +287,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift index 49392be769..8f8397d26e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift @@ -307,6 +307,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -323,6 +324,8 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -373,6 +376,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { dateReactions: dateReactionsAndPeers.reactions, dateReactionPeers: dateReactionsAndPeers.peers, dateReplies: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, dateText: dateText ) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index 332531f153..0017cedcf4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -1054,6 +1054,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -1067,6 +1068,8 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -1125,6 +1128,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift index 8d5e633293..01a336ad60 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -56,6 +56,7 @@ public class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNod var viewCount: Int? var rawText = "" var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -71,6 +72,8 @@ public class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNod if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -140,6 +143,7 @@ public class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNod areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index c968aa1a50..e2d44addfe 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -602,6 +602,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { var edited = false var viewCount: Int? = nil var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -615,6 +616,8 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -648,6 +651,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { areReactionsTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.message.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.message), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index f6e820a9af..1e0dd952d7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -264,6 +264,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.topMessage) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -278,6 +279,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -647,6 +650,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && (!item.associatedData.isInPinnedListMode || isReplyThread), hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift index 9bf11f830f..c7beb06383 100644 --- a/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift @@ -393,7 +393,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe let disclaimerText: NSMutableAttributedString if let verification = item.verification { - disclaimerText = NSMutableAttributedString(string: " # \(verification.description)", font: Font.regular(13.0), textColor: subtitleColor) + disclaimerText = NSMutableAttributedString(string: " # \(verification.description)", font: Font.regular(13.0), textColor: subtitleColor) if let range = disclaimerText.string.range(of: "#") { disclaimerText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: verification.iconFileId, file: nil), range: NSRange(range, in: disclaimerText.string)) disclaimerText.addAttribute(.foregroundColor, value: subtitleColor, range: NSRange(range, in: disclaimerText.string)) diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index b684b61345..ef5ec71c1e 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -1237,6 +1237,11 @@ final class GiftOptionsScreenComponent: Component { if availableProducts.isEmpty { var premiumProducts: [PremiumGiftProduct] = [] for option in premiumOptions { + if option.currency == "XTR" { + continue + } + let starsGiftOption = premiumOptions.first(where: { $0.currency == "XTR" && $0.months == option.months }) + premiumProducts.append( PremiumGiftProduct( giftOption: CachedPremiumGiftOption( @@ -1246,7 +1251,7 @@ final class GiftOptionsScreenComponent: Component { botUrl: "", storeProductId: option.storeProductId ), - starsGiftOption: nil, + starsGiftOption: starsGiftOption, storeProduct: nil, discount: nil ) diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 56549057c4..ece1fa0f97 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -498,16 +498,8 @@ final class GiftSetupScreenComponent: Component { starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) - let _ = (starsContext.state - |> take(until: { value in - if let value { - if !value.flags.contains(.isPendingBalance) { - return SignalTakeAction(passthrough: true, complete: true) - } - } - return SignalTakeAction(passthrough: false, complete: false) - }) - |> deliverOnMainQueue).start(next: { _ in + let _ = (starsContext.onUpdate + |> deliverOnMainQueue).start(next: { proceed() }) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 1ca31db8c7..da56c538e7 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -351,8 +351,12 @@ private final class GiftViewSheetContent: CombinedComponent { self.inProgress = true self.updated() + if let controller = self.getController() as? GiftViewScreen { + controller.showBalance = false + } + self.upgradeDisposable = (self.upgradeGift(formId, self.keepOriginalInfo) - |> deliverOnMainQueue).start(next: { [weak self] result in + |> deliverOnMainQueue).start(next: { [weak self, weak starsContext] result in guard let self, let controller = self.getController() as? GiftViewScreen else { return } @@ -363,6 +367,10 @@ private final class GiftViewSheetContent: CombinedComponent { controller.subject = self.subject controller.animateSuccess() self.updated(transition: .spring(duration: 0.4)) + + Queue.mainQueue().after(0.5) { + starsContext?.load(force: true) + } }) } @@ -382,11 +390,18 @@ private final class GiftViewSheetContent: CombinedComponent { starsContext: starsContext, options: options ?? [], purpose: .upgradeStarGift(requiredStars: price), - completion: { [weak starsContext] stars in - starsContext?.add(balance: StarsAmount(value: stars, nanos: 0)) - Queue.mainQueue().after(2.0) { - proceed(upgradeForm.id) + completion: { [weak self, weak starsContext] stars in + guard let self, let starsContext else { + return } + self.inProgress = true + self.updated() + + starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) + let _ = (starsContext.onUpdate + |> deliverOnMainQueue).start(next: { + proceed(upgradeForm.id) + }) } ) controller.push(purchaseController) @@ -2226,9 +2241,8 @@ private final class GiftViewSheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - 16.0 - buttons.size.width / 2.0, y: 28.0)) ) - let contentSize = CGSize(width: context.availableSize.width, height: originY + 5.0 + environment.safeInsets.bottom) - - return contentSize + let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom + return CGSize(width: context.availableSize.width, height: originY + 5.0 + effectiveBottomInset) } } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift index 6c81f40c57..9b7bc718ff 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift @@ -19,6 +19,7 @@ public final class PeerInfoGiftsCoverComponent: Component { public let giftsContext: ProfileGiftsContext public let hasBackground: Bool public let avatarCenter: CGPoint + public let avatarSize: CGSize public let defaultHeight: CGFloat public let avatarTransitionFraction: CGFloat public let statusBarHeight: CGFloat @@ -34,6 +35,7 @@ public final class PeerInfoGiftsCoverComponent: Component { giftsContext: ProfileGiftsContext, hasBackground: Bool, avatarCenter: CGPoint, + avatarSize: CGSize, defaultHeight: CGFloat, avatarTransitionFraction: CGFloat, statusBarHeight: CGFloat, @@ -48,6 +50,7 @@ public final class PeerInfoGiftsCoverComponent: Component { self.giftsContext = giftsContext self.hasBackground = hasBackground self.avatarCenter = avatarCenter + self.avatarSize = avatarSize self.defaultHeight = defaultHeight self.avatarTransitionFraction = avatarTransitionFraction self.statusBarHeight = statusBarHeight @@ -71,6 +74,9 @@ public final class PeerInfoGiftsCoverComponent: Component { if lhs.avatarCenter != rhs.avatarCenter { return false } + if lhs.avatarSize != rhs.avatarSize { + return false + } if lhs.defaultHeight != rhs.defaultHeight { return false } @@ -213,14 +219,14 @@ public final class PeerInfoGiftsCoverComponent: Component { } excludeRects.append(CGRect(origin: CGPoint(x: 0.0, y: component.statusBarHeight), size: component.topLeftButtonsSize)) excludeRects.append(CGRect(origin: CGPoint(x: availableSize.width - component.topRightButtonsSize.width, y: component.statusBarHeight), size: component.topRightButtonsSize)) - excludeRects.append(CGRect(origin: CGPoint(x: floor((availableSize.width - component.titleWidth) / 2.0), y: avatarCenter.y + 56.0), size: CGSize(width: component.titleWidth, height: 72.0))) + excludeRects.append(CGRect(origin: CGPoint(x: floor((availableSize.width - component.titleWidth) / 2.0), y: avatarCenter.y + component.avatarSize.height / 2.0 + 6.0), size: CGSize(width: component.titleWidth, height: 100.0))) if component.bottomHeight > 0.0 { excludeRects.append(CGRect(origin: CGPoint(x: 0.0, y: component.defaultHeight - component.bottomHeight), size: CGSize(width: availableSize.width, height: component.bottomHeight))) } let positionGenerator = PositionGenerator( containerSize: CGSize(width: availableSize.width, height: component.defaultHeight), - centerFrame: CGSize(width: 100, height: 100).centered(around: avatarCenter), + centerFrame: component.avatarSize.centered(around: avatarCenter), exclusionZones: excludeRects, minimumDistance: 42.0, edgePadding: 5.0, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift index 3b08ca63bd..bba2f02c8e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift @@ -257,7 +257,7 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { for key in removeKeys { if let buttonNode = self.rightButtonNodes.removeValue(forKey: key) { if key == .moreSearchSort || key == .searchWithTags || key == .standaloneSearch { - buttonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak buttonNode] _ in + buttonNode.layer.animateAlpha(from: buttonNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak buttonNode] _ in buttonNode?.removeFromSupernode() }) buttonNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 1c0c8c45df..b1daf1a100 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -2289,6 +2289,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { } if !buttonKeys.isEmpty { backgroundDefaultHeight = 327.0 + if metrics.isTablet { + backgroundDefaultHeight += 60.0 + } } hasBackground = true } else if let peer { @@ -2352,6 +2355,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { giftsContext: profileGiftsContext, hasBackground: hasBackground, avatarCenter: apparentAvatarFrame.center, + avatarSize: apparentAvatarFrame.size, defaultHeight: backgroundDefaultHeight, avatarTransitionFraction: max(0.0, min(1.0, titleCollapseFraction + transitionFraction * 2.0)), statusBarHeight: statusBarHeight, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index e924b19dd6..c394a58b4e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -677,13 +677,14 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr let buttonSideInset = sideInset + 16.0 let buttonSize = CGSize(width: size.width - buttonSideInset * 2.0, height: 50.0) - var bottomPanelHeight = max(8.0, bottomInset) + buttonSize.height + 8.0 + let effectiveBottomInset = max(8.0, bottomInset) + var bottomPanelHeight = effectiveBottomInset + buttonSize.height + 8.0 if params.visibleHeight < 110.0 { scrollOffset -= bottomPanelHeight } let panelTransition = ComponentTransition.immediate - panelTransition.setFrame(view: panelButton.view, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - bottomInset - buttonSize.height - scrollOffset), size: buttonSize)) + panelTransition.setFrame(view: panelButton.view, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - effectiveBottomInset - buttonSize.height - scrollOffset), size: buttonSize)) panelTransition.setAlpha(view: panelButton.view, alpha: panelAlpha) let _ = panelButton.updateLayout(width: buttonSize.width, transition: .immediate) @@ -754,7 +755,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr if panelCheckView.superview == nil { self.view.addSubview(panelCheckView) } - panelCheckView.frame = CGRect(origin: CGPoint(x: floor((size.width - panelCheckSize.width) / 2.0), y: size.height - bottomInset - panelCheckSize.height - 11.0 - scrollOffset), size: panelCheckSize) + panelCheckView.frame = CGRect(origin: CGPoint(x: floor((size.width - panelCheckSize.width) / 2.0), y: size.height - effectiveBottomInset - panelCheckSize.height - 11.0 - scrollOffset), size: panelCheckSize) panelTransition.setAlpha(view: panelCheckView, alpha: panelAlpha) } panelButton.isHidden = true @@ -998,7 +999,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr }))) } - if canReorder { + if case .unique = gift.gift, canReorder { items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Context_Reorder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in c?.dismiss(completion: { [weak self] in guard let self else { diff --git a/submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent/BUILD b/submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent/BUILD index 7e5550cac0..006c97027a 100644 --- a/submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent/BUILD @@ -13,6 +13,7 @@ swift_library( "//submodules/AsyncDisplayKit", "//submodules/Display", "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", "//submodules/AccountContext", "//submodules/ComponentFlow", "//submodules/Components/MultilineTextComponent", diff --git a/submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent/Sources/StarsBalanceOverlayComponent.swift b/submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent/Sources/StarsBalanceOverlayComponent.swift index dec416f208..c304e8ca32 100644 --- a/submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent/Sources/StarsBalanceOverlayComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent/Sources/StarsBalanceOverlayComponent.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import Display import ComponentFlow +import SwiftSignalKit import TelegramCore import AccountContext import TelegramPresentationData @@ -38,6 +39,10 @@ public final class StarsBalanceOverlayComponent: Component { private let action = ComponentView() private var component: StarsBalanceOverlayComponent? + private var state: EmptyComponentState? + + private var balance: Int64 = 0 + private var balanceDisposable: Disposable? private var cachedChevronImage: (UIImage, PresentationTheme)? @@ -53,17 +58,43 @@ public final class StarsBalanceOverlayComponent: Component { fatalError("init(coder:) has not been implemented") } + deinit { + self.balanceDisposable?.dispose() + } + @objc private func tapped() { if let component = self.component { component.action() } } + private var isUpdating = false func update(component: StarsBalanceOverlayComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } self.component = component + if self.balanceDisposable == nil, let starsContext = component.context.starsContext { + self.balanceDisposable = (starsContext.state + |> map { state -> Int64 in + return state?.balance.value ?? 0 + } + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] balance in + guard let self else { + return + } + self.balance = balance + if !self.isUpdating { + self.state?.updated() + } + }) + } + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let balance = presentationStringsFormattedNumber(Int32(component.context.starsContext?.currentState?.balance.value ?? 0), presentationData.dateTimeFormat.groupingSeparator) + let balance = presentationStringsFormattedNumber(Int32(self.balance), presentationData.dateTimeFormat.groupingSeparator) let attributedText = parseMarkdownIntoAttributedString( presentationData.strings.StarsBalance_YourBalance("**⭐️\(balance)**").string, @@ -121,23 +152,27 @@ public final class StarsBalanceOverlayComponent: Component { if let textView = self.text.view { if textView.superview == nil { - self.addSubview(textView) + self.backgroundView.addSubview(textView) } textView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: 10.0), size: textSize) } if let actionView = self.action.view { if actionView.superview == nil { - self.addSubview(actionView) + self.backgroundView.addSubview(actionView) } actionView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - actionSize.width) / 2.0), y: 29.0), size: actionSize) } self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.opaqueBackgroundColor, transition: .immediate) self.backgroundView.update(size: size, cornerRadius: size.height / 2.0, transition: .immediate) - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: size)) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - size.width) / 2.0), y: 0.0), size: size)) - return size + return CGSize(width: availableSize.width, height: size.height) + } + + public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return self.backgroundView.frame.contains(point) } } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index b4066cab07..96093e6a99 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -1530,15 +1530,15 @@ private final class StarsTransactionSheetContent: CombinedComponent { .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) ) originY += button.size.height + originY += 7.0 } context.add(closeButton .position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0)) ) - let contentSize = CGSize(width: context.availableSize.width, height: originY + 5.0 + environment.safeInsets.bottom) - - return contentSize + let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom + return CGSize(width: context.availableSize.width, height: originY + 5.0 + effectiveBottomInset) } } } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 46cbbfaf0a..d556d7a17c 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -589,10 +589,15 @@ private final class SheetContent: CombinedComponent { options: state?.options ?? [], purpose: purpose, completion: { [weak starsContext] stars in - starsContext?.add(balance: StarsAmount(value: stars, nanos: 0)) - Queue.mainQueue().after(0.1) { - completion() + guard let starsContext else { + return } + starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) + + let _ = (starsContext.onUpdate + |> deliverOnMainQueue).start(next: { + completion() + }) } ) controller?.push(purchaseController) diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/StarsCount.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/StarsCount.imageset/Contents.json new file mode 100644 index 0000000000..fa834c36d7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/StarsCount.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "msgstar.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/StarsCount.imageset/msgstar.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/StarsCount.imageset/msgstar.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5565014e2c1a06bad09ab7db408d9b2e8cdb0573 GIT binary patch literal 4264 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 deliverOnMainQueue).start(next: { value in let _ = currentCountriesConfiguration.swap(CountriesConfiguration(countries: value)) }) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index 3477609073..583e2babda 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -13,6 +13,7 @@ import PresentationDataUtils import UndoUI import UrlHandling import TelegramPresentationData +import ChatInterfaceState func openWebAppImpl( context: AccountContext, @@ -182,7 +183,7 @@ func openWebAppImpl( var isInline = false var botId = botPeer.id var botName = botName - var botAddress = "" + var botAddress = botPeer.addressName ?? "" var botVerified = botPeer.isVerified if case let .inline(bot) = source { isInline = true @@ -367,8 +368,20 @@ public extension ChatControllerImpl { } } } + let inputString = "@\(botAddress) \(query)" if let chatController { - chatController.controllerInteraction?.activateSwitchInline(selectedPeer?.id ?? peerId, "@\(botAddress) \(query)", nil) + chatController.controllerInteraction?.activateSwitchInline(selectedPeer?.id ?? peerId, inputString, nil) + } else if let selectedPeer, let navigationController = context.sharedContext.mainWindow?.viewController as? NavigationController { + let textInputState = ChatTextInputState(inputText: NSAttributedString(string: inputString)) + let _ = (ChatInterfaceState.update(engine: context.engine, peerId: selectedPeer.id, threadId: nil, { currentState in + return currentState.withUpdatedComposeInputState(textInputState) + }) + |> deliverOnMainQueue).startStandalone(completed: { [weak navigationController] in + guard let navigationController else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(selectedPeer), subject: nil, updateTextInputState: textInputState, peekData: nil)) + }) } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift index cae3d5b3d9..16d06cfde1 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift @@ -72,7 +72,10 @@ extension ChatControllerImpl { return } let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: options, purpose: .sendMessage(peerId: peer.id, requiredStars: totalAmount), completion: { _ in - completion(false) + let _ = (starsContext.onUpdate + |> deliverOnMainQueue).start(next: { + completion(false) + }) }) self.push(controller) }) diff --git a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift index 7eac909522..adedeb9680 100644 --- a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift @@ -3,6 +3,7 @@ import UIKit import Display import AsyncDisplayKit import Postbox +import SwiftSignalKit import TelegramCore import TelegramPresentationData import LocalizedPeerData @@ -14,6 +15,7 @@ import TextNodeWithEntities import AnimationCache import MultiAnimationRenderer import AccountContext +import PremiumUI private enum ChatReportPeerTitleButton: Equatable { case block @@ -344,7 +346,7 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { private let context: AccountContext private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer - + private let separatorNode: ASDisplayNode private let closeButton: HighlightableButtonNode @@ -354,12 +356,17 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { private let emojiSeparatorNode: ASDisplayNode private var theme: PresentationTheme? + private var presentationInterfaceState: ChatPresentationInterfaceState? private var inviteInfoNode: ChatInfoTitlePanelInviteInfoNode? private var peerNearbyInfoNode: ChatInfoTitlePanelPeerNearbyInfoNode? private var cachedChevronImage: (UIImage, PresentationTheme)? + private var emojiStatusPackDisposable = MetaDisposable() + private var emojiStatusFileId: Int64? + private var emojiStatusFileAndPackTitle = Promise<(TelegramMediaFile, LoadedStickerPack)?>() + private var tapGestureRecognizer: UITapGestureRecognizer? init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) { @@ -391,6 +398,10 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { self.addSubnode(self.closeButton) } + deinit { + self.emojiStatusPackDisposable.dispose() + } + override func didLoad() { super.didLoad() @@ -405,27 +416,33 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { } private func openPremiumEmojiStatusDemo() { - guard let navigationController = self.interfaceInteraction?.getNavigationController() else { + guard let navigationController = self.interfaceInteraction?.getNavigationController(), let peerId = self.presentationInterfaceState?.chatLocation.peerId, let emojiStatus = self.presentationInterfaceState?.renderedPeer?.peer?.emojiStatus, case let .emoji(fileId) = emojiStatus.content else { return } - if self.context.isPremium { - let controller = context.sharedContext.makePremiumIntroController(context: self.context, source: .animatedEmoji, forceDark: false, dismissed: nil) - navigationController.pushViewController(controller) - } else { - var replaceImpl: ((ViewController) -> Void)? - let controller = self.context.sharedContext.makePremiumDemoController(context: self.context, subject: .emojiStatus, forceDark: false, action: { [weak self] in - guard let self else { - return - } - let controller = context.sharedContext.makePremiumIntroController(context: self.context, source: .animatedEmoji, forceDark: false, dismissed: nil) - replaceImpl?(controller) - }, dismissed: nil) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) + let source: Signal = self.emojiStatusFileAndPackTitle.get() + |> take(1) + |> mapToSignal { emojiStatusFileAndPack -> Signal in + if let (file, pack) = emojiStatusFileAndPack { + return .single(.emojiStatus(peerId, fileId, file, pack)) + } else { + return .complete() } - navigationController.pushViewController(controller) } + + let _ = (source + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak navigationController] source in + guard let self, let navigationController else { + return + } + let controller = PremiumIntroScreen(context: self.context, source: source) + if let textView = self.emojiStatusTextNode?.view { + controller.sourceView = textView + controller.sourceRect = CGRect(origin: .zero, size: CGSize(width: textView.frame.height, height: textView.frame.height)) + } + controller.containerView = navigationController.view + navigationController.pushViewController(controller) + }) } override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult { @@ -436,7 +453,8 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor self.emojiSeparatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor } - + self.presentationInterfaceState = interfaceState + var panelHeight: CGFloat = 40.0 let contentRightInset: CGFloat = 14.0 + rightInset @@ -583,11 +601,43 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { } } - /*#if DEBUG - emojiStatus = PeerEmojiStatus(fileId: 5062172592505356289, expirationDate: nil) - #endif*/ - - if let emojiStatus = emojiStatus { + if let emojiStatus = emojiStatus, case let .emoji(fileId) = emojiStatus.content { + if self.emojiStatusFileId != fileId { + self.emojiStatusFileId = fileId + + let emojiFileAndPack = self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]) + |> mapToSignal { result in + if let emojiFile = result.first?.value { + for attribute in emojiFile.attributes { + if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { + return self.context.engine.stickers.loadedStickerPack(reference: packReference, forceActualized: false) + |> filter { result in + if case .result = result { + return true + } else { + return false + } + } + |> mapToSignal { result -> Signal<(TelegramMediaFile, LoadedStickerPack)?, NoError> in + if case let .result(_, items, _) = result { + return .single(items.first.flatMap { ($0.file._parse(), result) }) + } else { + return .complete() + } + } + } + } + } + return .complete() + } + self.emojiStatusPackDisposable.set(emojiFileAndPack.startStrict(next: { [weak self] fileAndPackTitle in + guard let self else { + return + } + self.emojiStatusFileAndPackTitle.set(.single(fileAndPackTitle)) + })) + } + self.emojiSeparatorNode.isHidden = false transition.updateFrame(node: self.emojiSeparatorNode, frame: CGRect(origin: CGPoint(x: leftInset + 12.0, y: 40.0), size: CGSize(width: width - leftInset - rightInset - 24.0, height: UIScreenPixel))) From a67c0b41aff8feb59a39b0eefa77f9c06a9cb2a0 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 12 Mar 2025 00:40:37 +0400 Subject: [PATCH 16/19] Bump version --- versions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions.json b/versions.json index 26fd2ba0c2..9146b480e6 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "11.8.1", + "app": "11.8.2", "xcode": "16.2", "bazel": "7.3.1:981f82a470bad1349322b6f51c9c6ffa0aa291dab1014fac411543c12e661dff", "macos": "15" From 369116767a9a0a96fe61f55d3013436eef6792fa Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 12 Mar 2025 18:57:22 +0400 Subject: [PATCH 17/19] Various fixes --- .../Sources/Node/ChatListItem.swift | 2 +- .../Sources/PeerInfoCoverComponent.swift | 10 +- .../Sources/PeerInfoGiftsCoverComponent.swift | 101 +++++++++++------- ...oHeaderNavigationButtonContainerNode.swift | 5 +- .../Sources/PeerInfoHeaderNode.swift | 1 + .../Sources/PeerInfoGiftsPaneNode.swift | 4 +- .../TelegramUI/Sources/ChatController.swift | 5 +- .../WebUI/Sources/WebAppController.swift | 3 + 8 files changed, 82 insertions(+), 49 deletions(-) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 3c2c57a158..8fb4bff944 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -4098,7 +4098,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.secretIconNode = iconNode } iconNode.image = currentSecretIconImage - transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.origin.y + floor((titleLayout.size.height - currentSecretIconImage.size.height) / 2.0)), size: currentSecretIconImage.size)) + transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + titleLeftOffset, y: contentRect.origin.y + floor((titleLayout.size.height - currentSecretIconImage.size.height) / 2.0)), size: currentSecretIconImage.size)) titleOffset += currentSecretIconImage.size.width + 3.0 } else if let secretIconNode = strongSelf.secretIconNode { strongSelf.secretIconNode = nil diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift index baf1702c7e..09faa26854 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift @@ -111,6 +111,7 @@ public final class PeerInfoCoverComponent: Component { public let files: [Int64: TelegramMediaFile] public let isDark: Bool public let avatarCenter: CGPoint + public let avatarSize: CGSize public let avatarScale: CGFloat public let defaultHeight: CGFloat public let gradientOnTop: Bool @@ -124,6 +125,7 @@ public final class PeerInfoCoverComponent: Component { files: [Int64: TelegramMediaFile], isDark: Bool, avatarCenter: CGPoint, + avatarSize: CGSize = CGSize(width: 100.0, height: 100.0), avatarScale: CGFloat, defaultHeight: CGFloat, gradientOnTop: Bool = false, @@ -136,6 +138,7 @@ public final class PeerInfoCoverComponent: Component { self.files = files self.isDark = isDark self.avatarCenter = avatarCenter + self.avatarSize = avatarSize self.avatarScale = avatarScale self.defaultHeight = defaultHeight self.gradientOnTop = gradientOnTop @@ -160,6 +163,9 @@ public final class PeerInfoCoverComponent: Component { if lhs.avatarCenter != rhs.avatarCenter { return false } + if lhs.avatarSize != rhs.avatarSize { + return false + } if lhs.avatarScale != rhs.avatarScale { return false } @@ -492,7 +498,7 @@ public final class PeerInfoCoverComponent: Component { transition.containedViewLayoutTransition.updateFrameAdditive(view: self.backgroundPatternContainer, frame: backgroundPatternContainerFrame) transition.setAlpha(view: self.backgroundPatternContainer, alpha: component.patternTransitionFraction) - var baseDistance: CGFloat = 72.0 + var baseDistance: CGFloat = component.avatarSize.width / 2.0 + 22.0 var baseRowDistance: CGFloat = 28.0 var baseItemSize: CGFloat = 26.0 if availableSize.width <= 60.0 { @@ -516,7 +522,7 @@ public final class PeerInfoCoverComponent: Component { let baseItemDistance: CGFloat = baseDistance + CGFloat(row) * baseRowDistance let itemDistanceFraction = max(0.0, min(1.0, baseItemDistance / (baseDistance * 2.0))) - let itemScaleFraction = patternScaleValueAt(fraction: component.avatarTransitionFraction, t: itemDistanceFraction, reverse: false) + let itemScaleFraction = patternScaleValueAt(fraction: component.avatarTransitionFraction * 1.6, t: itemDistanceFraction, reverse: false) let itemDistance = baseItemDistance * (1.0 - itemScaleFraction) + 20.0 * itemScaleFraction var itemAngle: CGFloat diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift index 9b7bc718ff..66f1bba085 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift @@ -304,26 +304,28 @@ public final class PeerInfoGiftsCoverComponent: Component { } iconLayer.glowing = component.hasBackground - let centerPosition = component.avatarCenter - let finalPosition = iconPosition.center.offsetBy(dx: component.avatarCenter.x, dy: component.avatarCenter.y) - let itemScaleFraction = patternScaleValueAt(fraction: component.avatarTransitionFraction, t: 0.0, reverse: false) + let itemDistanceFraction = max(0.0, min(0.5, (iconPosition.distance - component.avatarSize.width / 2.0) / 144.0)) + let itemScaleFraction = patternScaleValueAt(fraction: min(1.0, component.avatarTransitionFraction * 1.33), t: itemDistanceFraction, reverse: false) - func interpolateRect(from: CGPoint, to: CGPoint, t: CGFloat) -> CGPoint { + func interpolatePosition(from: PositionGenerator.Position, to: PositionGenerator.Position, t: CGFloat) -> PositionGenerator.Position { let clampedT = max(0, min(1, t)) - let interpolatedX = from.x + (to.x - from.x) * clampedT - let interpolatedY = from.y + (to.y - from.y) * clampedT + let interpolatedDistance = from.distance + (to.distance - from.distance) * clampedT + let interpolatedAngle = from.angle + (to.angle - from.angle) * clampedT - return CGPoint( - x: interpolatedX, - y: interpolatedY - ) + return PositionGenerator.Position(distance: interpolatedDistance, angle: interpolatedAngle, scale: from.scale) } - let effectivePosition = interpolateRect(from: finalPosition, to: centerPosition, t: itemScaleFraction) + let toAngle: CGFloat = .pi * 0.18 + let centerPosition = PositionGenerator.Position(distance: 0.0, angle: iconPosition.angle + toAngle, scale: iconPosition.scale) + let effectivePosition = interpolatePosition(from: iconPosition, to: centerPosition, t: itemScaleFraction) + let effectiveAngle = toAngle * itemScaleFraction + let absolutePosition = getAbsolutePosition(position: effectivePosition, centerPoint: component.avatarCenter) + iconTransition.setBounds(layer: iconLayer, bounds: CGRect(origin: .zero, size: iconSize)) - iconTransition.setPosition(layer: iconLayer, position: effectivePosition) + iconTransition.setPosition(layer: iconLayer, position: absolutePosition) + iconLayer.updateRotation(effectiveAngle, transition: iconTransition) iconTransition.setScale(layer: iconLayer, scale: iconPosition.scale * (1.0 - itemScaleFraction)) iconTransition.setAlpha(layer: iconLayer, alpha: 1.0 - itemScaleFraction) @@ -586,7 +588,12 @@ private class GiftIconLayer: SimpleLayer { override func layoutSublayers() { self.shadowLayer.frame = CGRect(origin: .zero, size: self.bounds.size).insetBy(dx: -8.0, dy: -8.0) - self.animationLayer.frame = CGRect(origin: .zero, size: self.bounds.size) + self.animationLayer.bounds = CGRect(origin: .zero, size: self.bounds.size) + self.animationLayer.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0) + } + + func updateRotation(_ angle: CGFloat, transition: ComponentTransition) { + self.animationLayer.transform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0) } func startAnimations(index: Int) { @@ -644,8 +651,16 @@ private class GiftIconLayer: SimpleLayer { private struct PositionGenerator { struct Position { - let center: CGPoint + let distance: CGFloat + let angle: CGFloat let scale: CGFloat + + var relativeCartesian: CGPoint { + return CGPoint( + x: self.distance * cos(self.angle), + y: self.distance * sin(self.angle) + ) + } } let containerSize: CGSize @@ -706,15 +721,13 @@ private struct PositionGenerator { let orbitRangeSize = self.innerOrbitRange.max - self.innerOrbitRange.min let orbitDistanceFactor = self.innerOrbitRange.min + orbitRangeSize * CGFloat(self.lokiRng.next()) - let orbitDistance = orbitDistanceFactor * centerRadius + let distance = orbitDistanceFactor * centerRadius let angleRange: CGFloat = placeOnLeftSide ? .pi : .pi let angleOffset: CGFloat = placeOnLeftSide ? .pi/2 : -(.pi/2) let angle = angleOffset + angleRange * CGFloat(self.lokiRng.next()) - let absoluteX = centerPoint.x + orbitDistance * cos(angle) - let absoluteY = centerPoint.y + orbitDistance * sin(angle) - let absolutePosition = CGPoint(x: absoluteX, y: absoluteY) + let absolutePosition = getAbsolutePosition(distance: distance, angle: angle, centerPoint: centerPoint) if absolutePosition.x - itemSize.width/2 < self.edgePadding || absolutePosition.x + itemSize.width/2 > self.containerSize.width - self.edgePadding || @@ -723,11 +736,6 @@ private struct PositionGenerator { continue } - let relativePosition = CGPoint( - x: absolutePosition.x - centerPoint.x, - y: absolutePosition.y - centerPoint.y - ) - let itemRect = CGRect( x: absolutePosition.x - itemSize.width/2, y: absolutePosition.y - itemSize.height/2, @@ -735,10 +743,12 @@ private struct PositionGenerator { height: itemSize.height ) - if self.isValidPosition(itemRect, existingPositions: positions.map { self.posToAbsolute($0.center, centerPoint: centerPoint) }, itemSize: itemSize) { + if self.isValidPosition(itemRect, existingPositions: positions.map { + getAbsolutePosition(distance: $0.distance, angle: $0.angle, centerPoint: centerPoint) + }, itemSize: itemSize) { let scaleRangeSize = max(self.scaleRange.min + 0.1, 0.75) - self.scaleRange.max let scale = self.scaleRange.max + scaleRangeSize * CGFloat(self.lokiRng.next()) - positions.append(Position(center: relativePosition, scale: scale)) + positions.append(Position(distance: distance, angle: angle, scale: scale)) if absolutePosition.x < centerPoint.x { leftPositions += 1 @@ -757,16 +767,13 @@ private struct PositionGenerator { let orbitRangeSize = self.outerOrbitRange.max - self.outerOrbitRange.min let orbitDistanceFactor = self.outerOrbitRange.min + orbitRangeSize * CGFloat(self.lokiRng.next()) - let orbitDistance = orbitDistanceFactor * centerRadius + let distance = orbitDistanceFactor * centerRadius let angleRange: CGFloat = placeOnLeftSide ? .pi : .pi let angleOffset: CGFloat = placeOnLeftSide ? .pi/2 : -(.pi/2) let angle = angleOffset + angleRange * CGFloat(self.lokiRng.next()) - let absoluteX = centerPoint.x + orbitDistance * cos(angle) - let absoluteY = centerPoint.y + orbitDistance * sin(angle) - let absolutePosition = CGPoint(x: absoluteX, y: absoluteY) - + let absolutePosition = getAbsolutePosition(distance: distance, angle: angle, centerPoint: centerPoint) if absolutePosition.x - itemSize.width/2 < self.edgePadding || absolutePosition.x + itemSize.width/2 > self.containerSize.width - self.edgePadding || absolutePosition.y - itemSize.height/2 < self.edgePadding || @@ -774,11 +781,6 @@ private struct PositionGenerator { continue } - let relativePosition = CGPoint( - x: absolutePosition.x - centerPoint.x, - y: absolutePosition.y - centerPoint.y - ) - let itemRect = CGRect( x: absolutePosition.x - itemSize.width/2, y: absolutePosition.y - itemSize.height/2, @@ -786,12 +788,12 @@ private struct PositionGenerator { height: itemSize.height ) - if self.isValidPosition(itemRect, existingPositions: positions.map { self.posToAbsolute($0.center, centerPoint: centerPoint) }, itemSize: itemSize) { - let distance = hypot(absolutePosition.x - centerPoint.x, absolutePosition.y - centerPoint.y) - + if self.isValidPosition(itemRect, existingPositions: positions.map { + getAbsolutePosition(distance: $0.distance, angle: $0.angle, centerPoint: centerPoint) + }, itemSize: itemSize) { let normalizedDistance = min(distance / maxPossibleDistance, 1.0) let scale = self.scaleRange.max - normalizedDistance * (self.scaleRange.max - self.scaleRange.min) - positions.append(Position(center: relativePosition, scale: scale)) + positions.append(Position(distance: distance, angle: angle, scale: scale)) if absolutePosition.x < centerPoint.x { leftPositions += 1 @@ -804,8 +806,11 @@ private struct PositionGenerator { return positions } - private func posToAbsolute(_ relativePos: CGPoint, centerPoint: CGPoint) -> CGPoint { - return CGPoint(x: relativePos.x + centerPoint.x, y: relativePos.y + centerPoint.y) + func getAbsolutePosition(distance: CGFloat, angle: CGFloat, centerPoint: CGPoint) -> CGPoint { + return CGPoint( + x: centerPoint.x + distance * cos(angle), + y: centerPoint.y + distance * sin(angle) + ) } private func isValidPosition(_ rect: CGRect, existingPositions: [CGPoint], itemSize: CGSize) -> Bool { @@ -833,6 +838,20 @@ private struct PositionGenerator { } } +private func getAbsolutePosition(position: PositionGenerator.Position, centerPoint: CGPoint) -> CGPoint { + return CGPoint( + x: centerPoint.x + position.distance * cos(position.angle), + y: centerPoint.y + position.distance * sin(position.angle) + ) +} + +private func getAbsolutePosition(distance: CGFloat, angle: CGFloat, centerPoint: CGPoint) -> CGPoint { + return CGPoint( + x: centerPoint.x + distance * cos(angle), + y: centerPoint.y + distance * sin(angle) + ) +} + private func windowFunction(t: CGFloat) -> CGFloat { return bezierPoint(0.6, 0.0, 0.4, 1.0, t) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift index bba2f02c8e..98202da6c3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift @@ -74,6 +74,7 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { func update(size: CGSize, presentationData: PresentationData, leftButtons: [PeerInfoHeaderNavigationButtonSpec], rightButtons: [PeerInfoHeaderNavigationButtonSpec], expandFraction: CGFloat, shouldAnimateIn: Bool, transition: ContainedViewLayoutTransition) { let sideInset: CGFloat = 24.0 + let expandedSideInset: CGFloat = 16.0 let maximumExpandOffset: CGFloat = 14.0 let expandOffset: CGFloat = -expandFraction * maximumExpandOffset @@ -188,7 +189,7 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { self.currentRightButtons = rightButtons var nextRegularButtonOrigin = size.width - sideInset - 8.0 - var nextExpandedButtonOrigin = size.width - sideInset - 8.0 + var nextExpandedButtonOrigin = size.width - expandedSideInset for spec in rightButtons.reversed() { let buttonNode: PeerInfoHeaderNavigationButton var wasAdded = false @@ -268,7 +269,7 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { } } else { var nextRegularButtonOrigin = size.width - sideInset - 8.0 - var nextExpandedButtonOrigin = size.width - sideInset - 8.0 + var nextExpandedButtonOrigin = size.width - expandedSideInset for spec in rightButtons.reversed() { var key = spec.key diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index b1daf1a100..0033cb9aa8 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -2311,6 +2311,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { files: [:], isDark: presentationData.theme.overallDarkAppearance, avatarCenter: apparentAvatarFrame.center.offsetBy(dx: bannerInset, dy: 0.0), + avatarSize: apparentAvatarFrame.size, avatarScale: avatarScale, defaultHeight: backgroundDefaultHeight, gradientCenter: CGPoint(x: 0.5, y: buttonKeys.isEmpty ? 0.5 : 0.45), diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index c394a58b4e..d041bdd4f2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -379,7 +379,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr let optionSpacing: CGFloat = 10.0 let itemsSideInset = params.sideInset + 16.0 - let defaultItemsInRow = params.size.width > params.size.height ? 5 : 3 + let defaultItemsInRow = params.size.width > params.size.height || params.size.width > 414.0 ? 5 : 3 let itemsInRow = max(1, min(starsProducts.count, defaultItemsInRow)) let defaultOptionWidth = (params.size.width - itemsSideInset * 2.0 - optionSpacing * CGFloat(defaultItemsInRow - 1)) / CGFloat(defaultItemsInRow) let optionWidth = (params.size.width - itemsSideInset * 2.0 - optionSpacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow) @@ -613,7 +613,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } var bottomScrollInset: CGFloat = 0.0 - var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * (starsOptionSize.height + optionSpacing) - optionSpacing + topInset + 16.0 + var contentHeight = ceil(CGFloat(starsProducts.count) / CGFloat(defaultItemsInRow)) * (starsOptionSize.height + optionSpacing) - optionSpacing + topInset + 16.0 let size = params.size let sideInset = params.sideInset diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index f61918c4e7..c3e915a55e 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -5786,7 +5786,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case let .known(value) = cachedData.businessIntro { businessIntro = value } - sendPaidMessageStars = cachedData.sendPaidMessageStars + if case let .peer(peerId) = chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { + } else { + sendPaidMessageStars = cachedData.sendPaidMessageStars + } } else if let cachedData = peerView.cachedData as? CachedGroupData { var invitedBy: Peer? if let invitedByPeerId = cachedData.invitedBy { diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index ccef0a2cde..21a1fe7c2b 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -2232,6 +2232,9 @@ public final class WebAppController: ViewController, AttachmentContainable { self.webView?.sendEvent(name: "fullscreen_changed", data: paramsString) controller.isFullscreen = isFullscreen + if isFullscreen { + controller.requestAttachmentMenuExpansion() + } if let (layout, _) = self.validLayout, case .regular = layout.metrics.widthClass { if let snapshotView = self.webView?.snapshotView(afterScreenUpdates: false) { From eabf0985ef59bea5dde543427c80c9a223077046 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 12 Mar 2025 21:24:42 +0400 Subject: [PATCH 18/19] Various fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 1 + .../AccountContext/Sources/Premium.swift | 1 + .../Display/Source/TextAlertController.swift | 2 +- .../ChatMessagePaymentAlertController.swift | 16 ++- .../Sources/GiftOptionsScreen.swift | 134 ++++++++++++------ .../Components/Gifts/GiftViewScreen/BUILD | 1 + .../Sources/GiftTransferAlertController.swift | 15 +- .../Sources/GiftViewScreen.swift | 17 +-- .../Sources/PeerInfoGiftsPaneNode.swift | 21 ++- .../Sources/StarsPurchaseScreen.swift | 6 +- .../Sources/SharedAccountContext.swift | 130 +++++++++++------ 11 files changed, 225 insertions(+), 119 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 581f26d896..2e8b4b8d2b 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13573,6 +13573,7 @@ Sorry for the inconvenience."; "FolderLinkPreview.ToastFolderAddedTitleV2" = "Folder {folder} Added"; "Stars.Purchase.UpgradeStarGiftInfo" = "Buy Stars to upgrade your gift into a unique collectible."; +"Stars.Purchase.TransferStarGiftInfo" = "Buy Stars to transfer ownership of your unique collectible."; "Gift.Send.Upgrade" = "Make Unique for %@"; "Gift.Send.Upgrade.Info" = "Enable this to let %1$@ turn your gift into a unique collectible. [Learn More >]()"; diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 15277eccbc..743f40c3cb 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -136,6 +136,7 @@ public enum StarsPurchasePurpose: Equatable { case unlockMedia(requiredStars: Int64) case starGift(peerId: EnginePeer.Id, requiredStars: Int64) case upgradeStarGift(requiredStars: Int64) + case transferStarGift(requiredStars: Int64) case sendMessage(peerId: EnginePeer.Id, requiredStars: Int64) } diff --git a/submodules/Display/Source/TextAlertController.swift b/submodules/Display/Source/TextAlertController.swift index c8ddef511c..291446f04b 100644 --- a/submodules/Display/Source/TextAlertController.swift +++ b/submodules/Display/Source/TextAlertController.swift @@ -129,7 +129,7 @@ public final class TextAlertContentActionNode: HighlightableButtonNode { if let range = attributedString.string.range(of: "$") { attributedString.addAttribute(.attachment, value: UIImage(bundleImageName: "Item List/PremiumIcon")!, range: NSRange(range, in: attributedString.string)) attributedString.addAttribute(.foregroundColor, value: color, range: NSRange(range, in: attributedString.string)) - attributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: attributedString.string)) } self.setAttributedTitle(attributedString, for: []) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift index 151913193d..75270b57dc 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift @@ -317,18 +317,20 @@ private final class ChatMessagePaymentAlertContentNode: AlertContentNode, ASGest } } -private class ChatMessagePaymentAlertController: AlertController { +public class ChatMessagePaymentAlertController: AlertController { private let context: AccountContext? private let presentationData: PresentationData private weak var parentNavigationController: NavigationController? - + private let showBalance: Bool + private let balance = ComponentView() - init(context: AccountContext?, presentationData: PresentationData, contentNode: AlertContentNode, navigationController: NavigationController?) { + public init(context: AccountContext?, presentationData: PresentationData, contentNode: AlertContentNode, navigationController: NavigationController?, showBalance: Bool = true) { self.context = context self.presentationData = presentationData self.parentNavigationController = navigationController - + self.showBalance = showBalance + super.init(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) self.willDismiss = { [weak self] in @@ -350,16 +352,16 @@ private class ChatMessagePaymentAlertController: AlertController { } } - override func dismissAnimated() { + public override func dismissAnimated() { super.dismissAnimated() self.animateOut() } - override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - if let context = self.context, let _ = self.parentNavigationController { + if let context = self.context, let _ = self.parentNavigationController, self.showBalance { let insets = layout.insets(options: .statusBar) let balanceSize = self.balance.update( transition: .immediate, diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index ef5ec71c1e..b1c6b7a7ff 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -216,6 +216,8 @@ final class GiftOptionsScreenComponent: Component { private var starsStateDisposable: Disposable? private var starsState: StarsContext.State? + private let optionsPromise = Promise<[StarsTopUpOption]?>(nil) + private var component: GiftOptionsScreenComponent? private(set) weak var state: State? private var environment: EnvironmentType? @@ -508,59 +510,94 @@ final class GiftOptionsScreenComponent: Component { gift: transferGift, peer: peer, transferStars: gift.transferStars ?? 0, - commit: { [weak controller] in - let _ = (context.engine.payments.transferStarGift(prepaid: gift.transferStars == 0, reference: reference, peerId: peer.id) - |> deliverOnMainQueue).start() - - guard let controller, let navigationController = controller.navigationController as? NavigationController else { - return - } - - if peer.id.namespace == Namespaces.Peer.CloudChannel { - var controllers = navigationController.viewControllers - controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) } - var foundController = false - for controller in controllers.reversed() { - if let controller = controller as? PeerInfoScreen, controller.peerId == component.peerId { - foundController = true - break - } + navigationController: controller.navigationController as? NavigationController, + commit: { [weak self, weak controller] in + let proceed: (Bool) -> Void = { waitForTopUp in + if waitForTopUp, let starsContext = context.starsContext { + let _ = (starsContext.onUpdate + |> deliverOnMainQueue).start(next: { + let _ = (context.engine.payments.transferStarGift(prepaid: gift.transferStars == 0, reference: reference, peerId: peer.id) + |> deliverOnMainQueue).start() + }) + } else { + let _ = (context.engine.payments.transferStarGift(prepaid: gift.transferStars == 0, reference: reference, peerId: peer.id) + |> deliverOnMainQueue).start() } - if !foundController { - if let controller = context.sharedContext.makePeerInfoController( - context: context, - updatedPresentationData: nil, - peer: peer._asPeer(), - mode: .gifts, - avatarInitiallyExpanded: false, - fromChat: false, - requestsContext: nil - ) { - controllers.append(controller) - } + + guard let controller, let navigationController = controller.navigationController as? NavigationController else { + return } - navigationController.setViewControllers(controllers, animated: true) - } else { - var controllers = navigationController.viewControllers - controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) && !($0 is PeerInfoScreen) && !($0 is ContactSelectionController) } - var foundController = false - for controller in controllers.reversed() { - if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation { + + if peer.id.namespace == Namespaces.Peer.CloudChannel { + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) } + var foundController = false + for controller in controllers.reversed() { + if let controller = controller as? PeerInfoScreen, controller.peerId == component.peerId { + foundController = true + break + } + } + if !foundController { + if let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .gifts, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + controllers.append(controller) + } + } + navigationController.setViewControllers(controllers, animated: true) + } else { + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) && !($0 is PeerInfoScreen) && !($0 is ContactSelectionController) } + var foundController = false + for controller in controllers.reversed() { + if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation { + chatController.hintPlayNextOutgoingGift() + foundController = true + break + } + } + if !foundController { + let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) chatController.hintPlayNextOutgoingGift() - foundController = true - break + controllers.append(chatController) } + navigationController.setViewControllers(controllers, animated: true) } - if !foundController { - let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) - chatController.hintPlayNextOutgoingGift() - controllers.append(chatController) + if let completion = component.completion { + completion() } - navigationController.setViewControllers(controllers, animated: true) } - - if let completion = component.completion { - completion() + + if let self, let transferStars = gift.transferStars, transferStars > 0, let starsContext = context.starsContext, let starsState = self.starsState { + if starsState.balance < StarsAmount(value: transferStars, nanos: 0) { + let _ = (self.optionsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak controller] options in + let purchaseController = context.sharedContext.makeStarsPurchaseScreen( + context: context, + starsContext: starsContext, + options: options ?? [], + purpose: .transferStarGift(requiredStars: transferStars), + completion: { stars in + starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) + proceed(true) + } + ) + controller?.push(purchaseController) + }) + } else { + proceed(false) + } + } else { + proceed(false) } } ) @@ -590,6 +627,11 @@ final class GiftOptionsScreenComponent: Component { self.state?.updated() } }) + + if let state = component.starsContext.currentState, state.balance < StarsAmount(value: 100, nanos: 0) { + self.optionsPromise.set(component.context.engine.payments.starsTopUpOptions() + |> map(Optional.init)) + } } self.component = component diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD index ace4e1f471..66375077f3 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD @@ -49,6 +49,7 @@ swift_library( "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", "//submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent", "//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent", + "//submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift index ffa0d3ffb8..91ccd670ac 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift @@ -12,6 +12,7 @@ import AppBundle import AvatarNode import Markdown import GiftItemComponent +import ChatMessagePaymentAlertController private final class GiftTransferAlertContentNode: AlertContentNode { private let context: AccountContext @@ -251,7 +252,14 @@ private final class GiftTransferAlertContentNode: AlertContentNode { } } -public func giftTransferAlertController(context: AccountContext, gift: StarGift.UniqueGift, peer: EnginePeer, transferStars: Int64, commit: @escaping () -> Void) -> AlertController { +public func giftTransferAlertController( + context: AccountContext, + gift: StarGift.UniqueGift, + peer: EnginePeer, + transferStars: Int64, + navigationController: NavigationController?, + commit: @escaping () -> Void +) -> AlertController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings @@ -267,7 +275,6 @@ public func giftTransferAlertController(context: AccountContext, gift: StarGift. } var dismissImpl: ((Bool) -> Void)? - var contentNode: GiftTransferAlertContentNode? let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: buttonText, action: { dismissImpl?(true) commit() @@ -275,9 +282,9 @@ public func giftTransferAlertController(context: AccountContext, gift: StarGift. dismissImpl?(true) })] - contentNode = GiftTransferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peer: peer, title: title, text: text, actions: actions) + let contentNode = GiftTransferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peer: peer, title: title, text: text, actions: actions) - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) + let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode, navigationController: navigationController, showBalance: transferStars > 0) dismissImpl = { [weak controller] animated in if animated { controller?.dismissAnimated() diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index da56c538e7..469950558b 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -147,13 +147,7 @@ private final class GiftViewSheetContent: CombinedComponent { var keepOriginalInfo = false - private var optionsDisposable: Disposable? - private(set) var options: [StarsTopUpOption] = [] { - didSet { - self.optionsPromise.set(self.options) - } - } - private let optionsPromise = ValuePromise<[StarsTopUpOption]?>(nil) + private let optionsPromise = Promise<[StarsTopUpOption]?>(nil) init( context: AccountContext, @@ -269,13 +263,8 @@ private final class GiftViewSheetContent: CombinedComponent { } if let starsContext = context.starsContext, let state = starsContext.currentState, state.balance < StarsAmount(value: 100, nanos: 0) { - self.optionsDisposable = (context.engine.payments.starsTopUpOptions() - |> deliverOnMainQueue).start(next: { [weak self] options in - guard let self else { - return - } - self.options = options - }) + self.optionsPromise.set(context.engine.payments.starsTopUpOptions() + |> map(Optional.init)) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index d041bdd4f2..66026f44ab 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -1016,7 +1016,26 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr guard let self else { return } - let _ = self.context.engine.accountData.setStarGiftStatus(starGift: uniqueGift, expirationDate: nil).startStandalone() + if self.context.isPremium { + let _ = self.context.engine.accountData.setStarGiftStatus(starGift: uniqueGift, expirationDate: nil).startStandalone() + } else { + let text = strings.Gift_View_TooltipPremiumWearing + let tooltipController = UndoOverlayController( + presentationData: presentationData, + content: .premiumPaywall(title: nil, text: text, customUndoText: nil, timeout: nil, linkAction: nil), + position: .bottom, + animateInAsReplacement: false, + appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0), + action: { [weak self] action in + if let self, case .info = action { + let premiumController = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .messageEffects, forceDark: false, dismissed: nil) + self.parentController?.push(premiumController) + } + return false + } + ) + self.parentController?.present(tooltipController, in: .current) + } }) }))) } diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index cf2e01e40e..96ebd273a3 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -241,6 +241,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { textString = strings.Stars_Purchase_StarGiftInfo(component.peers.first?.value.compactDisplayTitle ?? "").string case .upgradeStarGift: textString = strings.Stars_Purchase_UpgradeStarGiftInfo + case .transferStarGift: + textString = strings.Stars_Purchase_TransferStarGiftInfo case let .sendMessage(peerId, _): if peerId.namespace == Namespaces.Peer.CloudUser { textString = strings.Stars_Purchase_SendMessageInfo(component.peers.first?.value.compactDisplayTitle ?? "").string @@ -828,7 +830,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { titleText = strings.Stars_Purchase_GetStars case .gift: titleText = strings.Stars_Purchase_GiftStars - case let .topUp(requiredStars, _), let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars), let .starGift(_, requiredStars), let .upgradeStarGift(requiredStars), let .sendMessage(_, requiredStars): + case let .topUp(requiredStars, _), let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars), let .starGift(_, requiredStars), let .upgradeStarGift(requiredStars), let .transferStarGift(requiredStars), let .sendMessage(_, requiredStars): titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars)) } @@ -1274,6 +1276,8 @@ private extension StarsPurchasePurpose { return requiredStars case let .upgradeStarGift(requiredStars): return requiredStars + case let .transferStarGift(requiredStars): + return requiredStars case let .sendMessage(_, requiredStars): return requiredStars default: diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index e8b79259a2..16fdd3eaf9 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2883,62 +2883,102 @@ public final class SharedAccountContextImpl: SharedAccountContext { } } + let optionsPromise = Promise<[StarsTopUpOption]?>(nil) + if let state = context.starsContext?.currentState, state.balance < StarsAmount(value: 100, nanos: 0) { + optionsPromise.set(context.engine.payments.starsTopUpOptions() + |> map(Optional.init)) + } + presentTransferAlertImpl = { [weak controller] peer in guard let controller, case let .starGiftTransfer(_, _, gift, transferStars, _, _) = source else { return } - let alertController = giftTransferAlertController(context: context, gift: gift, peer: peer, transferStars: transferStars, commit: { [weak controller] in - completion?([peer.id]) - - guard let controller, let navigationController = controller.navigationController as? NavigationController else { - return - } - var controllers = navigationController.viewControllers - controllers = controllers.filter { !($0 is ContactSelectionController) } - - if !isChannelGift { - if peer.id.namespace == Namespaces.Peer.CloudChannel { - if let controller = context.sharedContext.makePeerInfoController( - context: context, - updatedPresentationData: nil, - peer: peer._asPeer(), - mode: .gifts, - avatarInitiallyExpanded: false, - fromChat: false, - requestsContext: nil - ) { - controllers.append(controller) + let alertController = giftTransferAlertController( + context: context, + gift: gift, + peer: peer, + transferStars: transferStars, + navigationController: controller.navigationController as? NavigationController, + commit: { [weak controller] in + let proceed: (Bool) -> Void = { waitForTopUp in + completion?([peer.id]) + + guard let controller, let navigationController = controller.navigationController as? NavigationController else { + return } - } else { - var foundController = false - for controller in controllers.reversed() { - if let chatController = controller as? ChatController, case .peer(id: peer.id) = chatController.chatLocation { - chatController.hintPlayNextOutgoingGift() - foundController = true - break + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is ContactSelectionController) } + + if !isChannelGift { + if peer.id.namespace == Namespaces.Peer.CloudChannel { + if let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .gifts, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + controllers.append(controller) + } + } else { + var foundController = false + for controller in controllers.reversed() { + if let chatController = controller as? ChatController, case .peer(id: peer.id) = chatController.chatLocation { + chatController.hintPlayNextOutgoingGift() + foundController = true + break + } + } + if !foundController { + let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.default), params: nil) + chatController.hintPlayNextOutgoingGift() + controllers.append(chatController) + } } } - if !foundController { - let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.default), params: nil) - chatController.hintPlayNextOutgoingGift() - controllers.append(chatController) + navigationController.setViewControllers(controllers, animated: true) + + Queue.mainQueue().after(0.3) { + let tooltipController = UndoOverlayController( + presentationData: presentationData, + content: .forward(savedMessages: false, text: presentationData.strings.Gift_Transfer_Success("\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))", peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string), + elevatedLayout: false, + action: { _ in return true } + ) + if let lastController = controllers.last as? ViewController { + lastController.present(tooltipController, in: .window(.root)) + } } } - } - navigationController.setViewControllers(controllers, animated: true) - - Queue.mainQueue().after(0.3) { - let tooltipController = UndoOverlayController( - presentationData: presentationData, - content: .forward(savedMessages: false, text: presentationData.strings.Gift_Transfer_Success("\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))", peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string), - elevatedLayout: false, - action: { _ in return true } - ) - if let lastController = controllers.last as? ViewController { - lastController.present(tooltipController, in: .window(.root)) + + if transferStars > 0, let starsContext = context.starsContext, let starsState = starsContext.currentState { + if starsState.balance < StarsAmount(value: transferStars, nanos: 0) { + let _ = (optionsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak controller] options in + let purchaseController = context.sharedContext.makeStarsPurchaseScreen( + context: context, + starsContext: starsContext, + options: options ?? [], + purpose: .transferStarGift(requiredStars: transferStars), + completion: { stars in + starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) + proceed(true) + } + ) + controller?.push(purchaseController) + }) + } else { + proceed(false) + } + } else { + proceed(false) } } - }) + ) controller.present(alertController, in: .window(.root)) } From 399933bc5e681018a21941b27081142caed9cb0b Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 12 Mar 2025 22:19:43 +0400 Subject: [PATCH 19/19] Fix build --- submodules/TelegramUI/Sources/AccountContext.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index 5185dd0941..943fa81e14 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -361,7 +361,6 @@ public final class AccountContextImpl: AccountContext { let langCode = sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode self.currentCountriesConfiguration = Atomic(value: CountriesConfiguration(countries: loadCountryCodes())) if !temp { - let langCode = sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode let currentCountriesConfiguration = self.currentCountriesConfiguration self.countriesConfigurationDisposable = (self.engine.localization.getCountriesList(accountManager: sharedContext.accountManager, langCode: langCode) |> deliverOnMainQueue).start(next: { value in