diff --git a/.gitignore b/.gitignore index cb0aa6a14b..a9eb99ef70 100644 --- a/.gitignore +++ b/.gitignore @@ -68,4 +68,5 @@ build-input/* submodules/OpusBinding/SharedHeaders/* submodules/FFMpegBinding/SharedHeaders/* submodules/OpenSSLEncryptionProvider/SharedHeaders/* -buildServer.json +submodules/TelegramCore/FlatSerialization/Sources/* +buildServer.json \ No newline at end of file diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index e7b3c4abfb..c0129a94f0 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13819,12 +13819,12 @@ Sorry for the inconvenience."; "GroupInfo.Permissions.ChargeForMessages" = "Charge for Messages"; "GroupInfo.Permissions.ChargeForMessagesInfo" = "If you turn this on, regular members of the group will have to pay Stars to send messages."; "GroupInfo.Permissions.MessagePrice" = "SET YOUR PRICE PER MESSAGE"; -"GroupInfo.Permissions.MessagePriceInfo" = "Your group will receive 85% of the selected fee (%1$@) for each incoming message."; +"GroupInfo.Permissions.MessagePriceInfo" = "Your group will receive %1$@% of the selected fee (%2$@) for each incoming message."; "Privacy.Messages.ChargeForMessages" = "Charge for Messages"; "Privacy.Messages.ChargeForMessagesInfo" = "Charge a fee for messages from people outide your contacts or those you haven't messaged first."; "Privacy.Messages.MessagePrice" = "SET YOUR PRICE PER MESSAGE"; -"Privacy.Messages.MessagePriceInfo" = "Your will receive 85% of the selected fee (%1$@) for each incoming message."; +"Privacy.Messages.MessagePriceInfo" = "Your will receive %1$@% of the selected fee (%2$@) for each incoming message."; "Privacy.Messages.RemoveFeeHeader" = "EXCEPTIONS"; "Privacy.Messages.RemoveFee" = "Remove Fee"; @@ -13839,9 +13839,15 @@ Sorry for the inconvenience."; "Notification.PaidMessage.Stars_1" = "%@ Star"; "Notification.PaidMessage.Stars_any" = "%@ Stars"; +"Notification.PaidMessage.Messages_1" = "%@ message"; +"Notification.PaidMessage.Messages_any" = "%@ messages"; + "Notification.PaidMessage" = "%1$@ paid %2$@ to send a message"; "Notification.PaidMessageYou" = "You paid %1$@ to send a message"; +"Notification.PaidMessageMany" = "%1$@ paid %2$@ to send %3$@"; +"Notification.PaidMessageYouMany" = "You paid %1$@ to send %2$@"; + "Stars.Transfer.Terms" = "By purchasing you agree to the [Terms of Service]()."; "Stars.Transfer.Terms_URL" = "https://telegram.org/tos/stars"; @@ -13850,10 +13856,31 @@ Sorry for the inconvenience."; "Settings.Privacy.Messages.ValuePaid" = "Paid"; "Stars.Transaction.PaidMessage_1" = "Fee for %@ Message"; -"Stars.Transaction.PaidMessage_anu" = "Fee for %@ Messages"; +"Stars.Transaction.PaidMessage_any" = "Fee for %@ Messages"; "Stars.Transaction.PaidMessage.Text" = "You receive **%@%** of the price that you charge for each incoming message. [Change Fee >]()"; +"Stars.Transaction.Paid" = "Paid"; + "Stars.Intro.Transaction.PaidMessage_1" = "Fee for %@ Message"; "Stars.Intro.Transaction.PaidMessage_any" = "Fee for %@ Messages"; "Stars.Purchase.SendMessageInfo" = "Buy Stars to send a message to **%@**."; "Stars.Purchase.SendGroupMessageInfo" = "Buy Stars to send a message in **%@**."; + +"Gift.Options.Gift.Transfer" = "Transfer"; +"Gift.Options.Gift.Filter.MyGifts" = "My Gifts"; +"Gift.Options.Premium.OrStars" = "or %@"; + +"Gift.Send.PayWithStars" = "Pay with %@"; +"Gift.Send.PayWithStars.Info" = "Your balance is **%@**. [Get More Stars >]()"; + +"Chat.PanelCustomStatusShortInfo" = "%@ is a mark for [Premium subscribers >]()"; + +"Chat.InputTextPaidMessagePlaceholder" = "Message for %@"; + +"Privacy.Messages.Stars_1" = "%@ Star"; +"Privacy.Messages.Stars_any" = "%@ Stars"; +"Privacy.Messages.Unlock" = "Unlock with Telegram Premium"; + +"Premium.PaidMessages" = "Paid Messages"; +"Premium.PaidMessagesInfo" = "Charge a fee for messages from non-contacts or new senders."; +"Premium.PaidMessages.Proceed" = "About Telegram Premium"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index ce848cbac1..578ecd36cd 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1116,6 +1116,8 @@ public protocol SharedAccountContext: AnyObject { func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController + func makeIncomingMessagePrivacyScreen(context: AccountContext, value: GlobalPrivacySettings.NonContactChatsPrivacy, exceptions: SelectivePrivacySettings, update: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void) -> ViewController + func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) func makeAffiliateProgramSetupScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, mode: AffiliateProgramSetupScreenMode) -> Signal @@ -1351,20 +1353,50 @@ public struct StickersSearchConfiguration { public struct StarsSubscriptionConfiguration { static var defaultValue: StarsSubscriptionConfiguration { - return StarsSubscriptionConfiguration(maxFee: 2500, usdWithdrawRate: 1200) + return StarsSubscriptionConfiguration( + maxFee: 2500, + usdWithdrawRate: 1200, + paidMessageMaxAmount: 10000, + paidMessageCommissionPermille: 850, + paidMessagesAvailable: false + ) } + + public let maxFee: Int64 + public let usdWithdrawRate: Int64 + public let paidMessageMaxAmount: Int64 + public let paidMessageCommissionPermille: Int32 + public let paidMessagesAvailable: Bool - public let maxFee: Int64? - public let usdWithdrawRate: Int64? - - fileprivate init(maxFee: Int64?, usdWithdrawRate: Int64?) { + fileprivate init( + maxFee: Int64, + usdWithdrawRate: Int64, + paidMessageMaxAmount: Int64, + paidMessageCommissionPermille: Int32, + paidMessagesAvailable: Bool + ) { self.maxFee = maxFee self.usdWithdrawRate = usdWithdrawRate + self.paidMessageMaxAmount = paidMessageMaxAmount + self.paidMessageCommissionPermille = paidMessageCommissionPermille + self.paidMessagesAvailable = paidMessagesAvailable } public static func with(appConfiguration: AppConfiguration) -> StarsSubscriptionConfiguration { - if let data = appConfiguration.data, let value = data["stars_subscription_amount_max"] as? Double, let usdRate = data["stars_usd_withdraw_rate_x1000"] as? Double { - return StarsSubscriptionConfiguration(maxFee: Int64(value), usdWithdrawRate: Int64(usdRate)) + if let data = appConfiguration.data { + let maxFee = (data["stars_subscription_amount_max"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.maxFee + let usdWithdrawRate = (data["stars_usd_withdraw_rate_x1000"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.usdWithdrawRate + let paidMessageMaxAmount = (data["stars_paid_message_amount_max"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.paidMessageMaxAmount + let paidMessageCommissionPermille = (data["stars_paid_message_commission_permille"] as? Double).flatMap(Int32.init) ?? StarsSubscriptionConfiguration.defaultValue.paidMessageCommissionPermille + let paidMessagesAvailable = (data["stars_paid_messages_available"] as? Bool) ?? StarsSubscriptionConfiguration.defaultValue.paidMessagesAvailable + + return StarsSubscriptionConfiguration( + maxFee: maxFee, + usdWithdrawRate: usdWithdrawRate, + paidMessageMaxAmount: paidMessageMaxAmount, + paidMessageCommissionPermille: paidMessageCommissionPermille, + paidMessagesAvailable: paidMessagesAvailable + ) } else { return .defaultValue } diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 18bd99f8fd..15277eccbc 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -42,6 +42,7 @@ public enum PremiumIntroSource { case folderTags case animatedEmoji case messageEffects + case paidMessages } public enum PremiumGiftSource: Equatable { @@ -79,6 +80,7 @@ public enum PremiumDemoSubject { case folderTags case business case messageEffects + case paidMessages case businessLocation case businessHours @@ -134,6 +136,7 @@ public enum StarsPurchasePurpose: Equatable { case unlockMedia(requiredStars: Int64) case starGift(peerId: EnginePeer.Id, requiredStars: Int64) case upgradeStarGift(requiredStars: Int64) + case sendMessage(peerId: EnginePeer.Id, requiredStars: Int64) } public struct PremiumConfiguration { diff --git a/submodules/AccountContext/Sources/ShareController.swift b/submodules/AccountContext/Sources/ShareController.swift index 122c247bda..6e33940d3e 100644 --- a/submodules/AccountContext/Sources/ShareController.swift +++ b/submodules/AccountContext/Sources/ShareController.swift @@ -76,5 +76,5 @@ public enum ShareControllerSubject { case image([ImageRepresentationWithReference]) case media(AnyMediaReference, MediaParameters?) case mapMedia(TelegramMediaMap) - case fromExternal(([PeerId], [PeerId: Int64], String, ShareControllerAccountContext, Bool) -> Signal) + case fromExternal(Int, ([PeerId], [PeerId: Int64], [PeerId: StarsAmount], String, ShareControllerAccountContext, Bool) -> Signal) } diff --git a/submodules/AttachmentTextInputPanelNode/BUILD b/submodules/AttachmentTextInputPanelNode/BUILD index 5957166318..8539e94668 100644 --- a/submodules/AttachmentTextInputPanelNode/BUILD +++ b/submodules/AttachmentTextInputPanelNode/BUILD @@ -35,6 +35,7 @@ swift_library( "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities", "//submodules/TelegramUI/Components/Chat/ChatInputTextNode", + "//submodules/AnimatedCountLabelNode", ], visibility = [ "//visibility:public", diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputActionButtonsNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputActionButtonsNode.swift index 16b413b6ae..55f234013a 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputActionButtonsNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputActionButtonsNode.swift @@ -8,6 +8,7 @@ import ContextUI import ChatPresentationInterfaceState import ComponentFlow import AccountContext +import AnimatedCountLabelNode final class AttachmentTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageActionSheetControllerSourceSendButtonNode { private let strings: PresentationStrings @@ -17,7 +18,7 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode, ChatSendMessage let sendButton: HighlightTrackingButtonNode var sendButtonHasApplyIcon = false var animatingSendButton = false - let textNode: ImmediateTextNode + let textNode: ImmediateAnimatedCountLabelNode private var theme: PresentationTheme @@ -46,8 +47,7 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode, ChatSendMessage self.backgroundNode.clipsToBounds = true self.sendButton = HighlightTrackingButtonNode(pointerStyle: nil) - self.textNode = ImmediateTextNode() - self.textNode.attributedText = NSAttributedString(string: self.strings.MediaPicker_Send, font: Font.semibold(17.0), textColor: theme.chat.inputPanel.actionControlForegroundColor) + self.textNode = ImmediateAnimatedCountLabelNode() self.textNode.isUserInteractionEnabled = false super.init() @@ -104,8 +104,6 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode, ChatSendMessage func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) { self.backgroundNode.backgroundColor = theme.chat.inputPanel.actionControlFillColor - - self.textNode.attributedText = NSAttributedString(string: self.strings.MediaPicker_Send, font: Font.semibold(17.0), textColor: theme.chat.inputPanel.actionControlForegroundColor) } private var absoluteRect: (CGRect, CGSize)? @@ -113,20 +111,44 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode, ChatSendMessage self.absoluteRect = (rect, containerSize) } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, minimized: Bool, interfaceState: ChatPresentationInterfaceState) -> CGSize { + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, minimized: Bool, text: String, interfaceState: ChatPresentationInterfaceState) -> CGSize { self.validLayout = size let width: CGFloat - let textSize = self.textNode.updateLayout(CGSize(width: 100.0, height: 100.0)) + + var titleOffset: CGFloat = 0.0 + var segments: [AnimatedCountLabelNode.Segment] = [] + var buttonInset: CGFloat = 18.0 + if text.hasPrefix("⭐️") { + let font = Font.with(size: 17.0, design: .round, weight: .semibold, traits: .monospacedNumbers) + let badgeString = NSMutableAttributedString(string: "⭐️ ", font: font, textColor: interfaceState.theme.chat.inputPanel.actionControlForegroundColor) + if let range = badgeString.string.range(of: "⭐️") { + badgeString.addAttribute(.attachment, value: PresentationResourcesChat.chatPlaceholderStarIcon(interfaceState.theme)!, range: NSRange(range, in: badgeString.string)) + badgeString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: badgeString.string)) + } + segments.append(.text(0, badgeString)) + for char in text { + if let intValue = Int(String(char)) { + segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: interfaceState.theme.chat.inputPanel.actionControlForegroundColor))) + } + } + titleOffset -= 2.0 + buttonInset = 14.0 + } else { + segments.append(.text(0, NSAttributedString(string: text, font: Font.semibold(17.0), textColor: interfaceState.theme.chat.inputPanel.actionControlForegroundColor))) + } + self.textNode.segments = segments + + let textSize = self.textNode.updateLayout(size: CGSize(width: 100.0, height: 100.0), animated: transition.isAnimated) if minimized { width = 44.0 } else { - width = textSize.width + 36.0 + width = textSize.width + buttonInset * 2.0 } let buttonSize = CGSize(width: width, height: size.height) - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - textSize.width) / 2.0), y: floorToScreenPixels((buttonSize.height - textSize.height) / 2.0)), size: textSize)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - textSize.width) / 2.0) + titleOffset, y: floorToScreenPixels((buttonSize.height - textSize.height) / 2.0)), size: textSize)) transition.updateAlpha(node: self.textNode, alpha: minimized ? 0.0 : 1.0) transition.updateAlpha(node: self.sendButton.imageNode, alpha: minimized ? 1.0 : 0.0) diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index 2070783d8e..2d3f82233c 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -750,7 +750,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } self.theme = interfaceState.theme - + self.actionButtons.updateTheme(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) let textFieldMinHeight = calclulateTextFieldMinHeight(interfaceState, metrics: metrics) @@ -957,7 +957,17 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS var textBackgroundInset: CGFloat = 0.0 let actionButtonsSize: CGSize if let presentationInterfaceState = self.presentationInterfaceState { - actionButtonsSize = self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), transition: transition, minimized: !self.isAttachment || inputHasText, interfaceState: presentationInterfaceState) + let isMinimized: Bool + let text: String + if let sendPaidMessageStars = presentationInterfaceState.sendPaidMessageStars { + isMinimized = false + let count = max(1, presentationInterfaceState.interfaceState.forwardMessageIds?.count ?? 1) + text = "⭐️\(sendPaidMessageStars.value * Int64(count))" + } else { + isMinimized = !self.isAttachment || inputHasText + text = presentationInterfaceState.strings.MediaPicker_Send + } + actionButtonsSize = self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), transition: transition, minimized: isMinimized, text: text, interfaceState: presentationInterfaceState) textBackgroundInset = 44.0 - actionButtonsSize.width } else { actionButtonsSize = CGSize(width: 44.0, height: minimalHeight) diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index bd0202fb71..9f4a1610df 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -1141,7 +1141,7 @@ public class AttachmentController: ViewController, MinimizableController { } let isEffecitvelyCollapsedUpdated = (self.selectionCount > 0) != (self.panel.isSelecting) - let panelHeight = self.panel.update(layout: containerLayout, buttons: self.controller?.buttons ?? [], isSelecting: self.selectionCount > 0, elevateProgress: !hasPanel && !hasButton, transition: transition) + let panelHeight = self.panel.update(layout: containerLayout, buttons: self.controller?.buttons ?? [], isSelecting: self.selectionCount > 0, selectionCount: self.selectionCount, elevateProgress: !hasPanel && !hasButton, transition: transition) if hasPanel || hasButton { containerInsets.bottom = panelHeight } diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index ae45cff2b4..c318c99336 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -824,6 +824,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { private var presentationData: PresentationData private var updatedPresentationData: (initial: PresentationData, signal: Signal)? private var presentationDataDisposable: Disposable? + private var peerDisposable: Disposable? private var iconDisposables: [MediaId: Disposable] = [:] @@ -852,6 +853,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { private var buttons: [AttachmentButtonType] = [] private var selectedIndex: Int = 0 private(set) var isSelecting: Bool = false + private var selectionCount: Int = 0 + private var _isButtonVisible: Bool = false var isButtonVisible: Bool { return self.mainButtonState.isVisible || self.secondaryButtonState.isVisible @@ -1167,7 +1170,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], canMakePaidContent: canMakePaidContent, currentPrice: currentPrice, - hasTimers: hasTimers + hasTimers: hasTimers, + sendPaidMessageStars: strongSelf.presentationInterfaceState.sendPaidMessageStars )), hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, @@ -1258,14 +1262,35 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { strongSelf.updateChatPresentationInterfaceState({ $0.updatedTheme(presentationData.theme) }) if let layout = strongSelf.validLayout { - let _ = strongSelf.update(layout: layout, buttons: strongSelf.buttons, isSelecting: strongSelf.isSelecting, elevateProgress: strongSelf.elevateProgress, transition: .immediate) + let _ = strongSelf.update(layout: layout, buttons: strongSelf.buttons, isSelecting: strongSelf.isSelecting, selectionCount: strongSelf.selectionCount, elevateProgress: strongSelf.elevateProgress, transition: .immediate) } } }).strict() + + if let peerId = chatLocation?.peerId { + self.peerDisposable = ((self.context.account.viewTracker.peerView(peerId) + |> map { view -> StarsAmount? in + if let data = view.cachedData as? CachedUserData { + return data.sendPaidMessageStars + } else if let channel = peerViewMainPeer(view) as? TelegramChannel { + return channel.sendPaidMessageStars + } else { + return nil + } + } + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] amount in + guard let self else { + return + } + self.updateChatPresentationInterfaceState({ $0.updatedSendPaidMessageStars(amount) }) + })) + } } deinit { self.presentationDataDisposable?.dispose() + self.peerDisposable?.dispose() for (_, disposable) in self.iconDisposables { disposable.dispose() } @@ -1308,16 +1333,19 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { self.updateChatPresentationInterfaceState(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, f, completion: completion) } - private func updateChatPresentationInterfaceState(transition: ContainedViewLayoutTransition, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { + private func updateChatPresentationInterfaceState(update: Bool = true, transition: ContainedViewLayoutTransition, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { let presentationInterfaceState = f(self.presentationInterfaceState) + let updateInputTextState = self.presentationInterfaceState.interfaceState.effectiveInputState != presentationInterfaceState.interfaceState.effectiveInputState self.presentationInterfaceState = presentationInterfaceState - if let textInputPanelNode = self.textInputPanelNode, updateInputTextState { - textInputPanelNode.updateInputTextState(presentationInterfaceState.interfaceState.effectiveInputState, animated: transition.isAnimated) - - self.textUpdated(presentationInterfaceState.interfaceState.effectiveInputState.inputText) + if update { + if let textInputPanelNode = self.textInputPanelNode, updateInputTextState { + textInputPanelNode.updateInputTextState(presentationInterfaceState.interfaceState.effectiveInputState, animated: transition.isAnimated) + + self.textUpdated(presentationInterfaceState.interfaceState.effectiveInputState.inputText) + } } } @@ -1672,10 +1700,23 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { } } - func update(layout: ContainerViewLayout, buttons: [AttachmentButtonType], isSelecting: Bool, elevateProgress: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + func update(layout: ContainerViewLayout, buttons: [AttachmentButtonType], isSelecting: Bool, selectionCount: Int, elevateProgress: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { self.validLayout = layout self.buttons = buttons self.elevateProgress = elevateProgress + + if selectionCount != self.selectionCount { + self.selectionCount = selectionCount + self.updateChatPresentationInterfaceState(update: false, transition: .immediate, { state in + var selectedMessages: [EngineMessage.Id] = [] + for i in 0 ..< selectionCount { + selectedMessages.append(EngineMessage.Id(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: Int32(i))) + } + return state.updatedInterfaceState { state in + return state.withUpdatedForwardMessageIds(selectedMessages) + } + }) + } let isButtonVisibleUpdated = self._isButtonVisible != self.mainButtonState.isVisible self._isButtonVisible = self.mainButtonState.isVisible diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift index 9e87f7d610..c75dc8abf3 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift @@ -23,6 +23,7 @@ public enum SendMessageActionSheetControllerParams { public let canMakePaidContent: Bool public let currentPrice: Int64? public let hasTimers: Bool + public let sendPaidMessageStars: StarsAmount? public init( isScheduledMessages: Bool, @@ -34,7 +35,8 @@ public enum SendMessageActionSheetControllerParams { forwardMessageIds: [EngineMessage.Id], canMakePaidContent: Bool, currentPrice: Int64?, - hasTimers: Bool + hasTimers: Bool, + sendPaidMessageStars: StarsAmount? ) { self.isScheduledMessages = isScheduledMessages self.mediaPreview = mediaPreview @@ -46,6 +48,7 @@ public enum SendMessageActionSheetControllerParams { self.canMakePaidContent = canMakePaidContent self.currentPrice = currentPrice self.hasTimers = hasTimers + self.sendPaidMessageStars = sendPaidMessageStars } } diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift index 5d0bdb5453..93e5152744 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift @@ -455,6 +455,9 @@ final class ChatSendMessageContextScreenComponent: Component { if sendMessage.hasTimers { canSchedule = false } + if let _ = sendMessage.sendPaidMessageStars { + canSchedule = false + } canMakePaidContent = sendMessage.canMakePaidContent currentPrice = sendMessage.currentPrice case .editMessage: diff --git a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift index 7949ea417e..1f1d326fc9 100644 --- a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift +++ b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift @@ -30,6 +30,7 @@ public final class MultilineTextWithEntitiesComponent: Component { public let textShadowColor: UIColor? public let textStroke: (UIColor, CGFloat)? public let highlightColor: UIColor? + public let highlightInset: UIEdgeInsets public let handleSpoilers: Bool public let manualVisibilityControl: Bool public let resetAnimationsOnVisibilityChange: Bool @@ -53,6 +54,7 @@ public final class MultilineTextWithEntitiesComponent: Component { textShadowColor: UIColor? = nil, textStroke: (UIColor, CGFloat)? = nil, highlightColor: UIColor? = nil, + highlightInset: UIEdgeInsets = .zero, handleSpoilers: Bool = false, manualVisibilityControl: Bool = false, resetAnimationsOnVisibilityChange: Bool = false, @@ -75,6 +77,7 @@ public final class MultilineTextWithEntitiesComponent: Component { self.textShadowColor = textShadowColor self.textStroke = textStroke self.highlightColor = highlightColor + self.highlightInset = highlightInset self.highlightAction = highlightAction self.handleSpoilers = handleSpoilers self.manualVisibilityControl = manualVisibilityControl @@ -144,6 +147,10 @@ public final class MultilineTextWithEntitiesComponent: Component { return false } + if lhs.highlightInset != rhs.highlightInset { + return false + } + return true } @@ -189,6 +196,7 @@ public final class MultilineTextWithEntitiesComponent: Component { self.textNode.textShadowColor = component.textShadowColor self.textNode.textStroke = component.textStroke self.textNode.linkHighlightColor = component.highlightColor + self.textNode.linkHighlightInset = component.highlightInset self.textNode.highlightAttributeAction = component.highlightAction self.textNode.tapAttributeAction = component.tapAction self.textNode.longTapAttributeAction = component.longTapAction diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index b34a7042cb..553024fd8e 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -1793,6 +1793,25 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) } + if item.isAd { + let adButton: HighlightableButtonNode + if let current = strongSelf.adButton { + adButton = current + } else { + adButton = HighlightableButtonNode() + adButton.setImage(UIImage(bundleImageName: "Components/AdMock"), for: .normal) + strongSelf.addSubnode(adButton) + strongSelf.adButton = adButton + + adButton.addTarget(strongSelf, action: #selector(strongSelf.adButtonPressed), forControlEvents: .touchUpInside) + } + + adButton.frame = CGRect(origin: CGPoint(x: params.width - 20.0 - 31.0 - 13.0, y: 11.0), size: CGSize(width: 31.0, height: 15.0)) + } else if let adButton = strongSelf.adButton { + strongSelf.adButton = nil + adButton.removeFromSupernode() + } + strongSelf.updateEnableGestures() } }) diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index 7631e88947..10044328a6 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -368,6 +368,86 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusSize)) } + private func setupImageRecognition(_ generate: @escaping (TransformImageArguments) -> DrawingContext?, dimensions: PixelDimensions) { + guard let message = self.message, !message.isCopyProtected() && message.paidContent == nil else { + return + } + let displaySize = dimensions.cgSize.fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor + + self.recognitionDisposable.set((recognizedContent(context: self.context, image: { return generate(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))?.generateImage() }, messageId: message.id) + |> deliverOnMainQueue).start(next: { [weak self] results in + if let strongSelf = self { + strongSelf.recognizedContentNode?.removeFromSupernode() + if !results.isEmpty { + let size = strongSelf.imageNode.bounds.size + let recognizedContentNode = RecognizedContentContainer(size: size, recognitions: results, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, present: { [weak self] c, a in + if let strongSelf = self { + strongSelf.galleryController()?.presentInGlobalOverlay(c, with: a) + } + }, performAction: { [weak self] string, action in + guard let strongSelf = self else { + return + } + switch action { + case .copy: + UIPasteboard.general.string = string + if let controller = strongSelf.baseNavigationController()?.topViewController as? ViewController { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with({ $0 }) + let tooltipController = UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }) + controller.present(tooltipController, in: .window(.root)) + } + case .share: + if let controller = strongSelf.baseNavigationController()?.topViewController as? ViewController { + let shareController = ShareController(context: strongSelf.context, subject: .text(string), externalShare: true, immediateExternalShare: false, updatedPresentationData: (strongSelf.context.sharedContext.currentPresentationData.with({ $0 }), strongSelf.context.sharedContext.presentationData)) + controller.present(shareController, in: .window(.root)) + } + case .lookup: + let controller = UIReferenceLibraryViewController(term: string) + if let window = strongSelf.baseNavigationController()?.view.window { + controller.popoverPresentationController?.sourceView = window + controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0)) + window.rootViewController?.present(controller, animated: true) + } + case .speak: + if let speechHolder = speakText(context: strongSelf.context, text: string) { + speechHolder.completion = { [weak self, weak speechHolder] in + if let strongSelf = self, strongSelf.currentSpeechHolder == speechHolder { + strongSelf.currentSpeechHolder = nil + } + } + strongSelf.currentSpeechHolder = speechHolder + } + case .translate: + if let parentController = strongSelf.baseNavigationController()?.topViewController as? ViewController { + let controller = TranslateScreen(context: strongSelf.context, text: string, canCopy: true, fromLanguage: nil) + controller.pushController = { [weak parentController] c in + (parentController?.navigationController as? NavigationController)?._keepModalDismissProgress = true + parentController?.push(c) + } + controller.presentController = { [weak parentController] c in + parentController?.present(c, in: .window(.root)) + } + parentController.present(controller, in: .window(.root)) + } + } + }) + recognizedContentNode.barcodeAction = { [weak self] payload, rect in + guard let strongSelf = self, let message = strongSelf.message else { + return + } + strongSelf.footerContentNode.openActionOptions?(.url(url: payload, concealed: true), message) + } + recognizedContentNode.alpha = 0.0 + recognizedContentNode.frame = CGRect(origin: CGPoint(), size: size) + recognizedContentNode.update(size: strongSelf.imageNode.bounds.size, transition: .immediate) + strongSelf.imageNode.addSubnode(recognizedContentNode) + strongSelf.recognizedContentNode = recognizedContentNode + strongSelf.recognitionOverlayContentNode.transitionIn() + } + } + })) + } + fileprivate func setMessage(_ message: Message, displayInfo: Bool, translateToLanguage: String?, peerIsCopyProtected: Bool, isSecret: Bool) { self.message = message self.translateToLanguage = translateToLanguage @@ -392,83 +472,11 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { case .medium, .full: strongSelf.statusNodeContainer.isHidden = true - Queue.concurrentDefaultQueue().async { - if let message = strongSelf.message, !message.isCopyProtected() && !imageReference.media.flags.contains(.hasStickers) && message.paidContent == nil { - strongSelf.recognitionDisposable.set((recognizedContent(context: strongSelf.context, image: { return generate(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))?.generateImage() }, messageId: message.id) - |> deliverOnMainQueue).start(next: { [weak self] results in - if let strongSelf = self { - strongSelf.recognizedContentNode?.removeFromSupernode() - if !results.isEmpty { - let size = strongSelf.imageNode.bounds.size - let recognizedContentNode = RecognizedContentContainer(size: size, recognitions: results, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, present: { [weak self] c, a in - if let strongSelf = self { - strongSelf.galleryController()?.presentInGlobalOverlay(c, with: a) - } - }, performAction: { [weak self] string, action in - guard let strongSelf = self else { - return - } - switch action { - case .copy: - UIPasteboard.general.string = string - if let controller = strongSelf.baseNavigationController()?.topViewController as? ViewController { - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with({ $0 }) - let tooltipController = UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }) - controller.present(tooltipController, in: .window(.root)) - } - case .share: - if let controller = strongSelf.baseNavigationController()?.topViewController as? ViewController { - let shareController = ShareController(context: strongSelf.context, subject: .text(string), externalShare: true, immediateExternalShare: false, updatedPresentationData: (strongSelf.context.sharedContext.currentPresentationData.with({ $0 }), strongSelf.context.sharedContext.presentationData)) - controller.present(shareController, in: .window(.root)) - } - case .lookup: - let controller = UIReferenceLibraryViewController(term: string) - if let window = strongSelf.baseNavigationController()?.view.window { - controller.popoverPresentationController?.sourceView = window - controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0)) - window.rootViewController?.present(controller, animated: true) - } - case .speak: - if let speechHolder = speakText(context: strongSelf.context, text: string) { - speechHolder.completion = { [weak self, weak speechHolder] in - if let strongSelf = self, strongSelf.currentSpeechHolder == speechHolder { - strongSelf.currentSpeechHolder = nil - } - } - strongSelf.currentSpeechHolder = speechHolder - } - case .translate: - if let parentController = strongSelf.baseNavigationController()?.topViewController as? ViewController { - let controller = TranslateScreen(context: strongSelf.context, text: string, canCopy: true, fromLanguage: nil) - controller.pushController = { [weak parentController] c in - (parentController?.navigationController as? NavigationController)?._keepModalDismissProgress = true - parentController?.push(c) - } - controller.presentController = { [weak parentController] c in - parentController?.present(c, in: .window(.root)) - } - parentController.present(controller, in: .window(.root)) - } - } - }) - recognizedContentNode.barcodeAction = { [weak self] payload, rect in - guard let strongSelf = self, let message = strongSelf.message else { - return - } - strongSelf.footerContentNode.openActionOptions?(.url(url: payload, concealed: true), message) - } - recognizedContentNode.alpha = 0.0 - recognizedContentNode.frame = CGRect(origin: CGPoint(), size: size) - recognizedContentNode.update(size: strongSelf.imageNode.bounds.size, transition: .immediate) - strongSelf.imageNode.addSubnode(recognizedContentNode) - strongSelf.recognizedContentNode = recognizedContentNode - strongSelf.recognitionOverlayContentNode.transitionIn() - } - } - })) + if !imageReference.media.flags.contains(.hasStickers) { + Queue.concurrentDefaultQueue().async { + strongSelf.setupImageRecognition(generate, dimensions: largestSize.dimensions) } } - case .none, .blurred: strongSelf.statusNodeContainer.isHidden = false } @@ -819,6 +827,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { largestSize = PixelDimensions(width: largestSize.height, height: largestSize.width) } } + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() /*if largestSize.width > 2600 || largestSize.height > 2600 { @@ -833,7 +842,18 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { strongSelf.updateImageFromFile(path: data.path) })) } else {*/ - self.imageNode.setSignal(chatMessageImageFile(account: context.account, userLocation: userLocation, fileReference: fileReference, thumbnail: false), dispatchOnDisplayLink: false) + + let signal = chatMessageImageFile(account: context.account, userLocation: userLocation, fileReference: fileReference, thumbnail: false) + |> afterNext({ [weak self] generate in + guard let self else { + return + } + Queue.concurrentDefaultQueue().async { + self.setupImageRecognition(generate, dimensions: largestSize) + } + }) + + self.imageNode.setSignal(signal, dispatchOnDisplayLink: false) //} self.zoomableContent = (largestSize.cgSize, self.imageNode) diff --git a/submodules/GameUI/Sources/GameControllerNode.swift b/submodules/GameUI/Sources/GameControllerNode.swift index c0274d25b1..06923b10d1 100644 --- a/submodules/GameUI/Sources/GameControllerNode.swift +++ b/submodules/GameUI/Sources/GameControllerNode.swift @@ -144,7 +144,7 @@ final class GameControllerNode: ViewControllerTracingNode { if eventName == "share_game" || eventName == "share_score" { if let (botPeer, gameName) = self.shareData(), let addressName = botPeer.addressName, !addressName.isEmpty, !gameName.isEmpty { if eventName == "share_score" { - self.present(ShareController(context: self.context, subject: .fromExternal({ [weak self] peerIds, threadIds, text, account, _ in + self.present(ShareController(context: self.context, subject: .fromExternal(1, { [weak self] peerIds, threadIds, requireStars, text, account, _ in if let strongSelf = self, let message = strongSelf.message, let account = account as? ShareControllerAppAccountContext { let signals = peerIds.map { TelegramEngine(account: account.context.account).messages.forwardGameWithScore(messageId: message.id, to: $0, threadId: threadIds[$0], as: nil) } return .single(.preparing(false)) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift index e1b84e2123..ceb9f8df51 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift @@ -493,13 +493,10 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: if state.subscriptionEnabled { var label: String = "" if let subscriptionFee = state.subscriptionFee, subscriptionFee > StarsAmount.zero { - var usdRate = 0.012 - if let usdWithdrawRate = configuration.usdWithdrawRate { - usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0 - } + let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0 label = presentationData.strings.InviteLink_Create_FeePerMonth("≈\(formatTonUsdValue(subscriptionFee.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))").string } - entries.append(.subscriptionFee(presentationData.theme, presentationData.strings.InviteLink_Create_FeePlaceholder, isEditingEnabled, state.subscriptionFee, label, configuration.maxFee.flatMap({ StarsAmount(value: $0, nanos: 0) }))) + entries.append(.subscriptionFee(presentationData.theme, presentationData.strings.InviteLink_Create_FeePlaceholder, isEditingEnabled, state.subscriptionFee, label, StarsAmount(value: configuration.maxFee, nanos: 0))) } let infoText: String if let _ = invite, state.subscriptionEnabled { diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index c1191eb94c..e303fe8b3b 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -594,10 +594,7 @@ public final class InviteLinkViewController: ViewController { guard let peer else { return } - var usdRate = 0.012 - if let usdWithdrawRate = configuration.usdWithdrawRate { - usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0 - } + let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0 let subscriptionController = context.sharedContext.makeStarsSubscriptionScreen(context: context, peer: peer, pricing: pricing, importer: importer, usdRate: usdRate) self?.controller?.push(subscriptionController) }) @@ -834,11 +831,8 @@ public final class InviteLinkViewController: ViewController { context.account.postbox.loadedPeerWithId(adminId) ) |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, requestsState, creatorPeer in if let strongSelf = self { - var usdRate = 0.012 - if let usdWithdrawRate = configuration.usdWithdrawRate { - usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0 - } - + let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0 + var entries: [InviteLinkViewEntry] = [] entries.append(.link(presentationData.theme, invite)) diff --git a/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift b/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift index bae1cc72f9..10b25f28dc 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift @@ -347,6 +347,8 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode { iconFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - iconSize.width) / 2.0), y: floor((contentSize.height - iconSize.height) / 2.0)), size: iconSize) } strongSelf.imageNode.frame = iconFrame + } else { + strongSelf.imageNode.image = nil } strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel)) diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h index 971042d722..efdee05775 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h @@ -47,6 +47,8 @@ @property (nonatomic, readonly) bool inhibitEditing; +@property (nonatomic, assign) int64_t sendPaidMessageStars; + + (instancetype)contextForCaptionsOnly; - (SSignal *)imageSignalForItem:(NSObject *)item; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h index 68e5771469..16ad2d606d 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h @@ -17,6 +17,13 @@ @end +@protocol TGPhotoSendStarsButtonView + +- (void)updateFrame:(CGRect)frame; +- (CGSize)updateCount:(int64_t)count; + +@end + @protocol TGCaptionPanelView @@ -122,7 +129,7 @@ @property (nonatomic, copy) void (^ _Nullable editCover)(CGSize dimensions, void(^_Nonnull completion)(UIImage * _Nonnull)); - +- (UIView *_Nonnull)sendStarsButtonAction:(void(^_Nonnull)(void))action; - (UIView *_Nonnull)solidRoundedButton:(NSString *_Nonnull)title action:(void(^_Nonnull)(void))action; - (id _Nonnull)drawingAdapter:(CGSize)size originalSize:(CGSize)originalSize isVideo:(bool)isVideo isAvatar:(bool)isAvatar entitiesView:(UIView * _Nullable)entitiesView; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h index a5bee6a828..4860d82e10 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h @@ -2,6 +2,8 @@ #import +@protocol TGPhotoPaintStickersContext; + typedef NS_OPTIONS(NSUInteger, TGPhotoEditorTab) { TGPhotoEditorNoneTab = 0, TGPhotoEditorCropTab = 1 << 0, @@ -53,7 +55,9 @@ typedef enum @property (nonatomic, assign) TGPhotoEditorBackButton backButtonType; @property (nonatomic, assign) TGPhotoEditorDoneButton doneButtonType; -- (instancetype)initWithContext:(id)context backButton:(TGPhotoEditorBackButton)backButton doneButton:(TGPhotoEditorDoneButton)doneButton solidBackground:(bool)solidBackground; +@property (nonatomic, assign) int64_t sendPaidMessageStars; + +- (instancetype)initWithContext:(id)context backButton:(TGPhotoEditorBackButton)backButton doneButton:(TGPhotoEditorDoneButton)doneButton solidBackground:(bool)solidBackground stickersContext:(id)stickersContext; - (void)transitionInAnimated:(bool)animated; - (void)transitionInAnimated:(bool)animated transparent:(bool)transparent; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m index 5582fede5e..6144b3c4f0 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m @@ -429,13 +429,13 @@ TGPhotoEditorDoneButton doneButton = isScheduledMessages ? TGPhotoEditorDoneButtonSchedule : TGPhotoEditorDoneButtonSend; - _portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false]; + _portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false stickersContext:editingContext.sendPaidMessageStars > 0 ? stickersContext : nil]; _portraitToolbarView.cancelPressed = toolbarCancelPressed; _portraitToolbarView.donePressed = toolbarDonePressed; _portraitToolbarView.doneLongPressed = toolbarDoneLongPressed; [_wrapperView addSubview:_portraitToolbarView]; - _landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false]; + _landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false stickersContext:nil]; _landscapeToolbarView.cancelPressed = toolbarCancelPressed; _landscapeToolbarView.donePressed = toolbarDonePressed; _landscapeToolbarView.doneLongPressed = toolbarDoneLongPressed; @@ -1227,6 +1227,10 @@ if (_ignoreSelectionUpdates) return; + NSUInteger finalCount = MAX(1, selectedCount); + _portraitToolbarView.sendPaidMessageStars = (_editingContext.sendPaidMessageStars * finalCount); + [_portraitToolbarView setNeedsLayout]; + if (counterVisible) { bool animateCount = animated && !(counterVisible && _photoCounterButton.internalHidden); diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m index db0e8ca807..72bfc3cc6d 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m @@ -293,7 +293,7 @@ TGPhotoEditorBackButton backButton = TGPhotoEditorBackButtonCancel; TGPhotoEditorDoneButton doneButton = TGPhotoEditorDoneButtonCheck; - _portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:backButton doneButton:doneButton solidBackground:true]; + _portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:backButton doneButton:doneButton solidBackground:true stickersContext:nil]; [_portraitToolbarView setToolbarTabs:_availableTabs animated:false]; [_portraitToolbarView setActiveTab:_currentTab]; _portraitToolbarView.cancelPressed = toolbarCancelPressed; @@ -302,7 +302,7 @@ _portraitToolbarView.tabPressed = toolbarTabPressed; [_wrapperView addSubview:_portraitToolbarView]; - _landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:backButton doneButton:doneButton solidBackground:true]; + _landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:backButton doneButton:doneButton solidBackground:true stickersContext:nil]; [_landscapeToolbarView setToolbarTabs:_availableTabs animated:false]; [_landscapeToolbarView setActiveTab:_currentTab]; _landscapeToolbarView.cancelPressed = toolbarCancelPressed; diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorSliderView.m b/submodules/LegacyComponents/Sources/TGPhotoEditorSliderView.m index e32c11cdb5..4e2e5d3131 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorSliderView.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorSliderView.m @@ -141,7 +141,7 @@ const CGFloat TGPhotoEditorSliderViewInternalMargin = 7.0f; if (vertical) startPosition = 2 * visualMargin + visualTotalLength - startPosition; - CGFloat endPosition = visualMargin + visualTotalLength / (_maximumValue - _minimumValue) * (ABS(_minimumValue) + 1.0); + CGFloat endPosition = visualMargin + visualTotalLength / (_maximumValue - _minimumValue) * (ABS(_minimumValue) + _maximumValue); if (vertical) endPosition = 2 * visualMargin + visualTotalLength - endPosition; diff --git a/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m b/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m index 8a8bd067e1..d982cf6297 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m +++ b/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m @@ -10,6 +10,8 @@ #import "TGMediaAssetsController.h" +#import "TGPhotoPaintStickersContext.h" + @interface TGPhotoToolbarView () { id _context; @@ -19,6 +21,7 @@ UIView *_buttonsWrapperView; TGModernButton *_cancelButton; TGModernButton *_doneButton; + UIView *_starsDoneButton; UILabel *_infoLabel; @@ -32,7 +35,7 @@ @implementation TGPhotoToolbarView -- (instancetype)initWithContext:(id)context backButton:(TGPhotoEditorBackButton)backButton doneButton:(TGPhotoEditorDoneButton)doneButton solidBackground:(bool)solidBackground +- (instancetype)initWithContext:(id)context backButton:(TGPhotoEditorBackButton)backButton doneButton:(TGPhotoEditorDoneButton)doneButton solidBackground:(bool)solidBackground stickersContext:(id)stickersContext { self = [super initWithFrame:CGRectZero]; if (self != nil) @@ -56,12 +59,24 @@ [_cancelButton addTarget:self action:@selector(cancelButtonPressed) forControlEvents:UIControlEventTouchUpInside]; [_backgroundView addSubview:_cancelButton]; - _doneButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, buttonSize.width, buttonSize.height)]; - _doneButton.exclusiveTouch = true; - _doneButton.adjustsImageWhenHighlighted = false; - [self setDoneButtonType:doneButton]; - [_doneButton addTarget:self action:@selector(doneButtonPressed) forControlEvents:UIControlEventTouchUpInside]; - [_backgroundView addSubview:_doneButton]; + if (stickersContext != nil) { + __weak TGPhotoToolbarView *weakSelf = self; + _starsDoneButton = [stickersContext sendStarsButtonAction:^{ + __strong TGPhotoToolbarView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + [strongSelf doneButtonPressed]; + }]; + _starsDoneButton.exclusiveTouch = true; + [_backgroundView addSubview:_starsDoneButton]; + } else { + _doneButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, buttonSize.width, buttonSize.height)]; + _doneButton.exclusiveTouch = true; + _doneButton.adjustsImageWhenHighlighted = false; + [self setDoneButtonType:doneButton]; + [_doneButton addTarget:self action:@selector(doneButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [_backgroundView addSubview:_doneButton]; + } _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(doneButtonLongPressed:)]; _longPressGestureRecognizer.minimumPressDuration = 0.4; @@ -704,6 +719,11 @@ if (_doneButton.frame.size.width > 49.0f) offset = 60.0f; + if (_starsDoneButton != nil) { + CGSize buttonSize = [_starsDoneButton updateCount:_sendPaidMessageStars]; + [_starsDoneButton updateFrame:CGRectMake(self.frame.size.width - buttonSize.width - 2.0, 49.0f - offset + 2.0f, buttonSize.width, buttonSize.height)]; + } + _doneButton.frame = CGRectMake(self.frame.size.width - offset, 49.0f - offset, _doneButton.frame.size.width, _doneButton.frame.size.height); _infoLabel.frame = CGRectMake(49.0f + 10.0f, 0.0f, self.frame.size.width - (49.0f + 10.0f) * 2.0f, 49.0f); diff --git a/submodules/LegacyMediaPickerUI/BUILD b/submodules/LegacyMediaPickerUI/BUILD index b919be5465..cfae8f3fe5 100644 --- a/submodules/LegacyMediaPickerUI/BUILD +++ b/submodules/LegacyMediaPickerUI/BUILD @@ -30,6 +30,7 @@ swift_library( "//submodules/DrawingUI:DrawingUI", "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", "//submodules/TelegramUI/Components/MediaEditor", + "//submodules/AnimatedCountLabelNode", ], visibility = [ "//visibility:public", diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index 10c395cb33..9f30f07350 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -11,6 +11,8 @@ import StickerResources import SolidRoundedButtonNode import MediaEditor import DrawingUI +import TelegramPresentationData +import AnimatedCountLabelNode protocol LegacyPaintEntity { var position: CGPoint { get } @@ -607,12 +609,109 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon return button } + public func sendStarsButtonAction(_ action: @escaping () -> Void) -> any UIView & TGPhotoSendStarsButtonView { + let button = SendStarsButtonView() + button.pressed = action + return button + } + public func drawingEntitiesView(with size: CGSize) -> UIView & TGPhotoDrawingEntitiesView { let view = DrawingEntitiesView(context: self.context, size: size) return view } } +private class SendStarsButtonView: HighlightTrackingButton, TGPhotoSendStarsButtonView { + private let backgroundView: UIView + private let textNode: ImmediateAnimatedCountLabelNode + + fileprivate var pressed: (() -> Void)? + + override init(frame: CGRect) { + self.backgroundView = UIView() + + self.textNode = ImmediateAnimatedCountLabelNode() + self.textNode.isUserInteractionEnabled = false + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + self.addSubview(self.textNode.view) + + self.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if highlighted { + self.backgroundView.layer.removeAnimation(forKey: "opacity") + self.backgroundView.alpha = 0.4 + self.textNode.layer.removeAnimation(forKey: "opacity") + self.textNode.alpha = 0.4 + } else { + self.backgroundView.alpha = 1.0 + self.backgroundView.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) + } + } + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + func updateFrame(_ frame: CGRect) { + let transition: ContainedViewLayoutTransition + if self.frame.width.isZero { + transition = .immediate + } else { + transition = .animated(duration: 0.4, curve: .spring) + } + transition.updateFrame(view: self, frame: frame) + } + + func updateCount(_ count: Int64) -> CGSize { + let text = "\(count)" + let transition: ContainedViewLayoutTransition + if self.backgroundView.frame.width.isZero { + transition = .immediate + } else { + transition = .animated(duration: 0.4, curve: .spring) + } + + var segments: [AnimatedCountLabelNode.Segment] = [] + let font = Font.with(size: 17.0, design: .round, weight: .semibold, traits: .monospacedNumbers) + let badgeString = NSMutableAttributedString(string: "⭐️ ", font: font, textColor: .white) + if let range = badgeString.string.range(of: "⭐️") { + badgeString.addAttribute(.attachment, value: PresentationResourcesChat.chatPlaceholderStarIcon(defaultDarkPresentationTheme)!, range: NSRange(range, in: badgeString.string)) + badgeString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: badgeString.string)) + } + segments.append(.text(0, badgeString)) + for char in text { + if let intValue = Int(String(char)) { + segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: .white))) + } + } + + self.textNode.segments = segments + + let buttonInset: CGFloat = 14.0 + let textSize = self.textNode.updateLayout(size: CGSize(width: 100.0, height: 100.0), animated: transition.isAnimated) + let width = textSize.width + buttonInset * 2.0 + let buttonSize = CGSize(width: width, height: 45.0) + let titleOffset: CGFloat = 0.0 + + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - textSize.width) / 2.0) + titleOffset, y: floorToScreenPixels((buttonSize.height - textSize.height) / 2.0)), size: textSize)) + + let backgroundSize = CGSize(width: width - 11.0, height: 33.0) + transition.updateFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - backgroundSize.width) / 2.0), y: floorToScreenPixels((buttonSize.height - backgroundSize.height) / 2.0)), size: backgroundSize)) + self.backgroundView.layer.cornerRadius = backgroundSize.height / 2.0 + self.backgroundView.backgroundColor = UIColor(rgb: 0x007aff) + + return buttonSize; + } +} + //Xcode 16 #if canImport(ContactProvider) extension SolidRoundedButtonView: @retroactive TGPhotoSolidRoundedButtonView { diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index e4becd9189..d7c4651c46 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -211,7 +211,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att public var openAvatarEditor: () -> Void = {} private var completed = false - public var legacyCompletion: (_ signals: [Any], _ silently: Bool, _ scheduleTime: Int32?, ChatSendMessageActionSheetController.SendParameters?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void = { _, _, _, _, _, _ in } + public var legacyCompletion: (_ fromGallery: Bool, _ signals: [Any], _ silently: Bool, _ scheduleTime: Int32?, ChatSendMessageActionSheetController.SendParameters?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void = { _, _, _, _, _, _, _ in } public var requestAttachmentMenuExpansion: () -> Void = { } public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } @@ -1294,7 +1294,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att } } - fileprivate func send(asFile: Bool = false, silently: Bool, scheduleTime: Int32?, animated: Bool, parameters: ChatSendMessageActionSheetController.SendParameters?, completion: @escaping () -> Void) { + fileprivate func send(fromGallery: Bool = false, asFile: Bool = false, silently: Bool, scheduleTime: Int32?, animated: Bool, parameters: ChatSendMessageActionSheetController.SendParameters?, completion: @escaping () -> Void) { guard let controller = self.controller, !controller.completed else { return } @@ -1334,7 +1334,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att return } controller.completed = true - controller.legacyCompletion(signals, silently, scheduleTime, parameters, { [weak self] identifier in + controller.legacyCompletion(fromGallery, signals, silently, scheduleTime, parameters, { [weak self] identifier in return !asFile ? self?.getItemSnapshot(identifier) : nil }, { [weak self] in completion() @@ -1834,6 +1834,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att paidMediaAllowed: Bool = false, subject: Subject, forCollage: Bool = false, + sendPaidMessageStars: Int64? = nil, editingContext: TGMediaEditingContext? = nil, selectionContext: TGMediaSelectionContext? = nil, saveEditedPhotos: Bool = false, @@ -2114,7 +2115,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att if let currentItem = currentItem { selectionState.setItem(currentItem, selected: true) } - strongSelf.controllerNode.send(silently: silently, scheduleTime: scheduleTime, animated: animated, parameters: parameters, completion: completion) + strongSelf.controllerNode.send(fromGallery: currentItem != nil, silently: silently, scheduleTime: scheduleTime, animated: animated, parameters: parameters, completion: completion) } }, schedule: { [weak self] parameters in if let strongSelf = self { @@ -2129,6 +2130,8 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att }, selectionState: selectionContext, editingState: editingContext ?? TGMediaEditingContext()) self.interaction?.selectionState?.grouping = true + self.interaction?.editingState.sendPaidMessageStars = sendPaidMessageStars ?? 0 + if case let .media(media) = self.subject { for item in media { selectionContext.setItem(item.asset, selected: true) diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift index b539c92717..86c02ce352 100644 --- a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift @@ -93,7 +93,7 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry { case chargeForMessagesInfo(PresentationTheme, String) case messagePriceHeader(PresentationTheme, String) - case messagePrice(PresentationTheme, StarsAmount, String) + case messagePrice(PresentationTheme, Int64, Int64, String) case messagePriceInfo(PresentationTheme, String) case unrestrictBoostersSwitch(PresentationTheme, String, Bool) @@ -241,8 +241,8 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry { } else { return false } - case let .messagePrice(lhsTheme, lhsValue, lhsPrice): - if case let .messagePrice(rhsTheme, rhsValue, rhsPrice) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsPrice == rhsPrice { + case let .messagePrice(lhsTheme, lhsValue, lhsMaxValue, lhsPrice): + if case let .messagePrice(rhsTheme, rhsValue, rhsMaxValue, rhsPrice) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsMaxValue == rhsMaxValue, lhsPrice == rhsPrice { return true } else { return false @@ -424,8 +424,8 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry { 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, price): - return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, minValue: 10, maxValue: 9000, value: value.value, price: price, sectionId: self.section, updated: { value in + 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)) }) case let .messagePriceInfo(_, value): @@ -720,23 +720,22 @@ private func channelPermissionsControllerEntries(context: AccountContext, presen entries.append(.conversionInfo(presentationData.theme, presentationData.strings.GroupInfo_Permissions_BroadcastConvertInfo(presentationStringsFormattedNumber(participantsLimit, presentationData.dateTimeFormat.groupingSeparator)).string)) } - let chargeEnabled = state.modifiedStarsAmount != nil - - entries.append(.chargeForMessages(presentationData.theme, presentationData.strings.GroupInfo_Permissions_ChargeForMessages, chargeEnabled)) - entries.append(.chargeForMessagesInfo(presentationData.theme, presentationData.strings.GroupInfo_Permissions_ChargeForMessagesInfo)) - - if chargeEnabled { - var price: String = "" - if let amount = state.modifiedStarsAmount { - var usdRate = 0.012 - if let usdWithdrawRate = configuration.usdWithdrawRate { - usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0 - } - price = "≈\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))" + if cachedData.flags.contains(.paidMessagesAvailable) && channel.hasPermission(.banMembers) { + let sendPaidMessageStars = state.modifiedStarsAmount?.value ?? (cachedData.sendPaidMessageStars?.value ?? 0) + let chargeEnabled = sendPaidMessageStars > 0 + entries.append(.chargeForMessages(presentationData.theme, presentationData.strings.GroupInfo_Permissions_ChargeForMessages, chargeEnabled)) + entries.append(.chargeForMessagesInfo(presentationData.theme, presentationData.strings.GroupInfo_Permissions_ChargeForMessagesInfo)) + + if chargeEnabled { + var price: String = "" + let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0 + + price = "≈\(formatTonUsdValue(sendPaidMessageStars, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))" + + entries.append(.messagePriceHeader(presentationData.theme, presentationData.strings.GroupInfo_Permissions_MessagePrice)) + entries.append(.messagePrice(presentationData.theme, sendPaidMessageStars, configuration.paidMessageMaxAmount, price)) + entries.append(.messagePriceInfo(presentationData.theme, presentationData.strings.GroupInfo_Permissions_MessagePriceInfo("\(configuration.paidMessageCommissionPermille / 10)", price).string)) } - entries.append(.messagePriceHeader(presentationData.theme, presentationData.strings.GroupInfo_Permissions_MessagePrice)) - entries.append(.messagePrice(presentationData.theme, state.modifiedStarsAmount ?? StarsAmount(value: 4000, nanos: 0), price)) - entries.append(.messagePriceInfo(presentationData.theme, presentationData.strings.GroupInfo_Permissions_MessagePriceInfo(price).string)) } let canSendText = !effectiveRightsFlags.contains(.banSendText) @@ -876,6 +875,9 @@ public func channelPermissionsController(context: AccountContext, updatedPresent let updateUnrestrictBoostersDisposable = MetaDisposable() actionsDisposable.add(updateUnrestrictBoostersDisposable) + let updateSendPaidMessageStarsDisposable = MetaDisposable() + actionsDisposable.add(updateSendPaidMessageStarsDisposable) + let peerView = Promise() peerView.set(sourcePeerId.get() |> mapToSignal(context.account.viewTracker.peerView)) @@ -1252,6 +1254,17 @@ public func channelPermissionsController(context: AccountContext, updatedPresent 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()) + }) }, toggleIsOptionExpanded: { flags in updateState { state in var state = state diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index b488cd4d3b..5a01f90490 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -1099,6 +1099,25 @@ private final class DemoSheetContent: CombinedComponent { ) ) + availableItems[.paidMessages] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.paidMessages, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: component.context, + position: .top, + videoFile: configuration.videos["paid_messages"], + decoration: .badgeStars + )), + title: strings.Premium_PaidMessages, + text: strings.Premium_PaidMessagesInfo, + textColor: textColor + ) + ) + ) + ) + let index: Int = 0 var items: [DemoPagerComponent.Item] = [] if let item = availableItems.first(where: { $0.value.content.id == component.subject as AnyHashable }) { @@ -1195,6 +1214,8 @@ private final class DemoSheetContent: CombinedComponent { text = strings.Premium_FolderTagsStandaloneInfo case .messageEffects: text = strings.Premium_MessageEffectsInfo + case .paidMessages: + text = strings.Premium_PaidMessagesInfo default: text = "" } @@ -1279,6 +1300,9 @@ private final class DemoSheetContent: CombinedComponent { case .emojiStatus: buttonText = strings.Premium_EmojiStatus_Proceed buttonAnimationName = "premium_unlock" + case .paidMessages: + buttonText = strings.Premium_PaidMessages_Proceed + buttonAnimationName = "premium_unlock" default: buttonText = strings.Common_OK } @@ -1468,6 +1492,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer { case business case folderTags case messageEffects + case paidMessages case businessLocation case businessHours @@ -1526,6 +1551,8 @@ public class PremiumDemoScreen: ViewControllerComponentContainer { return .folderTags case .messageEffects: return .messageEffects + case .paidMessages: + return .paidMessages case .businessLocation: return .businessLocation case .businessHours: diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index 99de9f72af..5d3c60997c 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -433,6 +433,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { UIColor(rgb: 0xdb374b), UIColor(rgb: 0xcb3e6d), UIColor(rgb: 0xbc4395), + UIColor(rgb: 0xbc4395), UIColor(rgb: 0xab4ac4), UIColor(rgb: 0xab4ac4), UIColor(rgb: 0xa34cd7), @@ -538,6 +539,8 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { demoSubject = .messagePrivacy case .messageEffects: demoSubject = .messageEffects + case .paidMessages: + demoSubject = .paidMessages case .business: demoSubject = .business default: diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 4b4d8d2593..a1585b91cf 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -302,6 +302,12 @@ public enum PremiumSource: Equatable { } else { return false } + case .paidMessages: + if case .messageEffects = rhs { + return true + } else { + return false + } } } @@ -349,6 +355,7 @@ public enum PremiumSource: Equatable { case messageTags case folderTags case messageEffects + case paidMessages var identifier: String? { switch self { @@ -442,6 +449,8 @@ public enum PremiumSource: Equatable { return "folder_tags" case .messageEffects: return "effects" + case .paidMessages: + return "paid_messages" } } } @@ -470,6 +479,7 @@ public enum PremiumPerk: CaseIterable { case business case folderTags case messageEffects + case paidMessages case businessLocation case businessHours @@ -504,7 +514,8 @@ public enum PremiumPerk: CaseIterable { .messagePrivacy, .folderTags, .business, - .messageEffects + .messageEffects, + .paidMessages ] } @@ -578,6 +589,8 @@ public enum PremiumPerk: CaseIterable { return "folder_tags" case .messageEffects: return "effects" + case .paidMessages: + return "paid_messages" case .business: return "business" case .businessLocation: @@ -647,6 +660,8 @@ public enum PremiumPerk: CaseIterable { return strings.Premium_Business case .messageEffects: return strings.Premium_MessageEffects + case .paidMessages: + return strings.Premium_PaidMessages case .businessLocation: return strings.Business_Location case .businessHours: @@ -714,6 +729,8 @@ public enum PremiumPerk: CaseIterable { return strings.Premium_BusinessInfo case .messageEffects: return strings.Premium_MessageEffectsInfo + case .paidMessages: + return strings.Premium_PaidMessagesInfo case .businessLocation: return strings.Business_LocationInfo case .businessHours: @@ -781,7 +798,8 @@ public enum PremiumPerk: CaseIterable { return "Premium/Perk/Business" case .messageEffects: return "Premium/Perk/MessageEffects" - + case .paidMessages: + return "Premium/Perk/PaidMessages" case .businessLocation: return "Premium/BusinessPerk/Location" case .businessHours: @@ -819,6 +837,7 @@ struct PremiumIntroConfiguration { .colors, .wallpapers, .profileBadge, + .paidMessages, .messagePrivacy, .advancedChatManagement, .noAds, @@ -867,6 +886,12 @@ struct PremiumIntroConfiguration { perks = PremiumIntroConfiguration.defaultValue.perks } + #if DEBUG + if !perks.contains(.paidMessages) { + perks.append(.paidMessages) + } + #endif + var businessPerks: [PremiumPerk] = [] if let values = data["business_promo_order"] as? [String] { for value in values { @@ -1859,6 +1884,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { UIColor(rgb: 0xdb374b), UIColor(rgb: 0xcb3e6d), UIColor(rgb: 0xbc4395), + UIColor(rgb: 0xbc4395), UIColor(rgb: 0xab4ac4), UIColor(rgb: 0xab4ac4), UIColor(rgb: 0xa34cd7), @@ -2092,6 +2118,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { demoSubject = .messagePrivacy case .messageEffects: demoSubject = .messageEffects + case .paidMessages: + demoSubject = .paidMessages case .business: demoSubject = .business let _ = ApplicationSpecificNotice.setDismissedBusinessBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift index 5fd01d8666..bbf34ff84e 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -842,6 +842,24 @@ public class PremiumLimitsListScreen: ViewController { ) ) ) + availableItems[.paidMessages] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.paidMessages, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: context, + position: .top, + videoFile: videos["paid_messages"], + decoration: .badgeStars + )), + title: strings.Premium_PaidMessages, + text: strings.Premium_PaidMessagesInfo, + textColor: textColor + ) + ) + ) + ) availableItems[.business] = DemoPagerComponent.Item( AnyComponentWithIdentity( id: PremiumDemoScreen.Subject.business, diff --git a/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift b/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift index b054f966e8..48e42c7283 100644 --- a/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift +++ b/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift @@ -75,8 +75,9 @@ public final class SelectablePeerNode: ASDisplayNode { private let avatarSelectionNode: ASImageNode private let avatarNodeContainer: ASDisplayNode private let avatarNode: AvatarNode - private var avatarBadgeBackground: UIImageView? + private var avatarBadgeOutline: UIImageView? private var avatarBadge: UIImageView? + private var avatarBadgeLabel: ImmediateTextView? private let onlineNode: PeerOnlineMarkerNode private var checkNode: CheckNode? private let textNode: ImmediateTextNode @@ -149,7 +150,7 @@ public final class SelectablePeerNode: ASDisplayNode { } } - public func setup(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer, requiresPremiumForMessaging: Bool, customTitle: String? = nil, iconId: Int64? = nil, iconColor: Int32? = nil, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) { + public func setup(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer, requiresPremiumForMessaging: Bool, requiresStars: Int64? = nil, customTitle: String? = nil, iconId: Int64? = nil, iconColor: Int32? = nil, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) { self.setup( accountPeerId: context.account.peerId, postbox: context.account.postbox, @@ -165,6 +166,7 @@ public final class SelectablePeerNode: ASDisplayNode { strings: strings, peer: peer, requiresPremiumForMessaging: requiresPremiumForMessaging, + requiresStars: requiresStars, customTitle: customTitle, iconId: iconId, iconColor: iconColor, @@ -184,7 +186,7 @@ public final class SelectablePeerNode: ASDisplayNode { self.avatarNode.playRepostAnimation() } - public func setup(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, energyUsageSettings: EnergyUsageSettings, contentSettings: ContentSettings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer, requiresPremiumForMessaging: Bool, customTitle: String? = nil, iconId: Int64? = nil, iconColor: Int32? = nil, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) { + public func setup(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, energyUsageSettings: EnergyUsageSettings, contentSettings: ContentSettings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer, requiresPremiumForMessaging: Bool, requiresStars: Int64? = nil, customTitle: String? = nil, iconId: Int64? = nil, iconColor: Int32? = nil, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) { let isFirstTime = self.peer == nil self.peer = peer guard let mainPeer = peer.chatMainPeer else { @@ -223,16 +225,68 @@ public final class SelectablePeerNode: ASDisplayNode { self.textNode.attributedText = NSAttributedString(string: customTitle ?? text, font: textFont, textColor: self.currentSelected ? self.theme.selectedTextColor : defaultColor, paragraphAlignment: .center) self.avatarNode.setPeer(accountPeerId: accountPeerId, postbox: postbox, network: network, contentSettings: contentSettings, theme: theme, peer: mainPeer, overrideImage: overrideImage, emptyColor: self.theme.avatarPlaceholderColor, clipStyle: isForum ? .roundedRect : .round, synchronousLoad: synchronousLoad) - if requiresPremiumForMessaging { - let avatarBadgeBackground: UIImageView - if let current = self.avatarBadgeBackground { - avatarBadgeBackground = current + if let requiresStars { + let avatarBadgeOutline: UIImageView + if let current = self.avatarBadgeOutline { + avatarBadgeOutline = current } else { - avatarBadgeBackground = UIImageView() - avatarBadgeBackground.image = PresentationResourcesChatList.shareAvatarPremiumLockBadgeBackground(theme) - avatarBadgeBackground.tintColor = theme.chatList.itemBackgroundColor - self.avatarBadgeBackground = avatarBadgeBackground - self.avatarNode.view.addSubview(avatarBadgeBackground) + avatarBadgeOutline = UIImageView() + avatarBadgeOutline.contentMode = .scaleToFill + avatarBadgeOutline.image = PresentationResourcesChatList.shareAvatarStarsLockBadgeBackground(theme) + avatarBadgeOutline.tintColor = theme.actionSheet.opaqueItemBackgroundColor + self.avatarBadgeOutline = avatarBadgeOutline + self.avatarNodeContainer.view.addSubview(avatarBadgeOutline) + } + + let avatarBadge: UIImageView + if let current = self.avatarBadge { + avatarBadge = current + } else { + avatarBadge = UIImageView() + avatarBadge.contentMode = .scaleToFill + avatarBadge.image = PresentationResourcesChatList.shareAvatarStarsLockBadgeInnerBackground(theme) + avatarBadge.tintColor = theme.actionSheet.controlAccentColor + self.avatarBadge = avatarBadge + self.avatarNodeContainer.view.addSubview(avatarBadge) + } + + let avatarBadgeLabel: ImmediateTextView + if let current = self.avatarBadgeLabel { + avatarBadgeLabel = current + } else { + avatarBadgeLabel = ImmediateTextView() + self.avatarBadgeLabel = avatarBadgeLabel + self.avatarNodeContainer.view.addSubview(avatarBadgeLabel) + } + + let badgeString = NSMutableAttributedString(string: "⭐️\(presentationStringsFormattedNumber(Int32(requiresStars), " "))", font: Font.with(size: 9.0, design: .round , weight: .bold), textColor: theme.list.itemCheckColors.foregroundColor) + if let range = badgeString.string.range(of: "⭐️") { + badgeString.addAttribute(.attachment, value: UIImage(bundleImageName: "Premium/SendStarsPeerBadgeStarIcon")!, range: NSRange(range, in: badgeString.string)) + badgeString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: badgeString.string)) + badgeString.addAttribute(.kern, value: -0.8, range: NSRange(badgeString.string.startIndex ..< badgeString.string.endIndex, in: badgeString.string)) + } + avatarBadgeLabel.attributedText = badgeString + + let avatarFrame = self.avatarNode.frame + let badgeSize = avatarBadgeLabel.updateLayout(avatarFrame.size) + var badgeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((avatarFrame.width - badgeSize.width) / 2.0) - (self.currentSelected ? 15.0 : 0.0), y: avatarFrame.height - 13.0), size: badgeSize) + let badgeBackgroundFrame = CGRect(origin: CGPoint(x: badgeFrame.minX - 2.0, y: badgeFrame.minY - 3.0 - UIScreenPixel), size: CGSize(width: badgeFrame.width + 4.0, height: 16.0)) + let badgeOutlineFrame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.minX - 2.0, y: badgeBackgroundFrame.minY - 2.0), size: CGSize(width: badgeBackgroundFrame.width + 4.0, height: 20.0)) + badgeFrame = badgeFrame.offsetBy(dx: -2.0, dy: 0.0) + + avatarBadge.frame = badgeBackgroundFrame + avatarBadgeOutline.frame = badgeOutlineFrame + avatarBadgeLabel.frame = badgeFrame + } else if requiresPremiumForMessaging { + let avatarBadgeOutline: UIImageView + if let current = self.avatarBadgeOutline { + avatarBadgeOutline = current + } else { + avatarBadgeOutline = UIImageView() + avatarBadgeOutline.image = PresentationResourcesChatList.shareAvatarPremiumLockBadgeBackground(theme) + avatarBadgeOutline.tintColor = theme.chatList.itemBackgroundColor + self.avatarBadgeOutline = avatarBadgeOutline + self.avatarNode.view.addSubview(avatarBadgeOutline) } let avatarBadge: UIImageView @@ -247,19 +301,23 @@ public final class SelectablePeerNode: ASDisplayNode { let avatarFrame = self.avatarNode.frame let badgeFrame = CGRect(origin: CGPoint(x: avatarFrame.width - 20.0, y: avatarFrame.height - 20.0), size: CGSize(width: 20.0, height: 20.0)) - let badgeBackgroundFrame = badgeFrame.insetBy(dx: -2.0 + UIScreenPixel, dy: -2.0 + UIScreenPixel) + let badgeBackgroundFrame = badgeFrame.insetBy(dx: -2.0, dy: -2.0) - avatarBadgeBackground.frame = badgeBackgroundFrame + avatarBadgeOutline.frame = badgeBackgroundFrame avatarBadge.frame = badgeFrame } else { - if let avatarBadgeBackground = self.avatarBadgeBackground { - self.avatarBadgeBackground = nil - avatarBadgeBackground.removeFromSuperview() + if let avatarBadgeOutline = self.avatarBadgeOutline { + self.avatarBadgeOutline = nil + avatarBadgeOutline.removeFromSuperview() } if let avatarBadge = self.avatarBadge { self.avatarBadge = nil avatarBadge.removeFromSuperview() } + if let avatarBadgeLabel = self.avatarBadgeLabel { + self.avatarBadgeLabel = nil + avatarBadgeLabel.removeFromSuperview() + } } let onlineLayout = self.onlineNode.asyncLayout() @@ -340,6 +398,19 @@ public final class SelectablePeerNode: ASDisplayNode { context.fillEllipse(in: bounds.insetBy(dx: 2.0, dy: 2.0)) } }) + + if let avatarBadgeLabel = self.avatarBadgeLabel, let avatarBadge = self.avatarBadge, let avatarBadgeOutline = self.avatarBadgeOutline { + avatarBadgeLabel.center = CGPoint(x: self.avatarNode.bounds.width / 2.0 - 17.0, y: avatarBadgeLabel.center.y) + avatarBadge.center = CGPoint(x: self.avatarNode.bounds.width / 2.0 - 15.0, y: avatarBadge.center.y) + avatarBadgeOutline.center = CGPoint(x: self.avatarNode.bounds.width / 2.0 - 15.0, y: avatarBadgeOutline.center.y) + + if animated { + avatarBadgeLabel.layer.animatePosition(from: CGPoint(x: 15.0, y: 0.0), to: .zero, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + avatarBadge.layer.animatePosition(from: CGPoint(x: 15.0, y: 0.0), to: .zero, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + avatarBadgeOutline.layer.animatePosition(from: CGPoint(x: 15.0, y: 0.0), to: .zero, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + } + if animated { self.avatarNode.layer.animateScale(from: 1.0, to: 0.866666, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) self.avatarSelectionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) @@ -355,6 +426,18 @@ public final class SelectablePeerNode: ASDisplayNode { } else { self.avatarSelectionNode.image = nil } + + if let avatarBadgeLabel = self.avatarBadgeLabel, let avatarBadge = self.avatarBadge, let avatarBadgeOutline = self.avatarBadgeOutline { + avatarBadgeLabel.center = CGPoint(x: self.avatarNode.bounds.width / 2.0 - 2.0, y: avatarBadgeLabel.center.y) + avatarBadge.center = CGPoint(x: self.avatarNode.bounds.width / 2.0, y: avatarBadge.center.y) + avatarBadgeOutline.center = CGPoint(x: self.avatarNode.bounds.width / 2.0, y: avatarBadgeOutline.center.y) + + if animated { + avatarBadgeLabel.layer.animatePosition(from: CGPoint(x: -15.0, y: 0.0), to: .zero, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + avatarBadge.layer.animatePosition(from: CGPoint(x: -15.0, y: 0.0), to: .zero, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + avatarBadgeOutline.layer.animatePosition(from: CGPoint(x: -15.0, y: 0.0), to: .zero, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + } } if selected { @@ -365,8 +448,8 @@ public final class SelectablePeerNode: ASDisplayNode { self.addSubnode(checkNode) let avatarFrame = self.avatarNode.frame - let checkSize = CGSize(width: 24.0, height: 24.0) - checkNode.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX - 10.0, y: avatarFrame.maxY - 18.0), size: checkSize) + let checkSize = CGSize(width: 22.0, height: 22.0) + checkNode.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX - 14.0, y: avatarFrame.maxY - 15.0), size: checkSize) checkNode.setSelected(true, animated: animated) } } else if let checkNode = self.checkNode { @@ -416,8 +499,8 @@ public final class SelectablePeerNode: ASDisplayNode { self.onlineNode.frame = CGRect(origin: CGPoint(x: avatarContainerFrame.maxX - self.onlineNode.frame.width - 2.0, y: avatarContainerFrame.maxY - self.onlineNode.frame.height - 2.0), size: self.onlineNode.frame.size) if let checkNode = self.checkNode { - let checkSize = CGSize(width: 24.0, height: 24.0) - checkNode.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX - 10.0, y: avatarFrame.maxY - 18.0), size: checkSize) + let checkSize = CGSize(width: 22.0, height: 22.0) + checkNode.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX - 14.0, y: avatarFrame.maxY - 15.0), size: checkSize) } } } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift index 5d056d9346..b3be316415 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift @@ -19,19 +19,22 @@ private final class IncomingMessagePrivacyScreenArguments { let disabledValuePressed: () -> Void let infoLinkAction: () -> Void let openExceptions: () -> Void + let openPremiumInfo: () -> Void init( context: AccountContext, updateValue: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void, disabledValuePressed: @escaping () -> Void, infoLinkAction: @escaping () -> Void, - openExceptions: @escaping () -> Void + openExceptions: @escaping () -> Void, + openPremiumInfo: @escaping () -> Void ) { self.context = context self.updateValue = updateValue self.disabledValuePressed = disabledValuePressed self.infoLinkAction = infoLinkAction self.openExceptions = openExceptions + self.openPremiumInfo = openPremiumInfo } } @@ -49,8 +52,8 @@ private enum GlobalAutoremoveEntry: ItemListNodeEntry { case optionChargeForMessages(value: GlobalPrivacySettings.NonContactChatsPrivacy, isEnabled: Bool) case footer(value: GlobalPrivacySettings.NonContactChatsPrivacy) case priceHeader - case price(value: Int64, price: String) - case priceInfo(value: String) + case price(value: Int64, maxValue: Int64, price: String, isEnabled: Bool) + case priceInfo(commission: Int32, value: String) case exceptionsHeader case exceptions(count: Int) case exceptionsInfo @@ -128,7 +131,7 @@ private enum GlobalAutoremoveEntry: ItemListNodeEntry { if case .paidMessages = value { isChecked = true } - return ItemListCheckboxItem(presentationData: presentationData, icon: isEnabled ? nil : generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/Lock"), color: presentationData.theme.list.itemSecondaryTextColor), iconPlacement: .check, title: presentationData.strings.Privacy_Messages_ChargeForMessages, style: .left, checked: isChecked, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, icon: isEnabled || isChecked ? nil : generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/Lock"), color: presentationData.theme.list.itemSecondaryTextColor), iconPlacement: .check, title: presentationData.strings.Privacy_Messages_ChargeForMessages, style: .left, checked: isChecked, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateValue(.paidMessages(StarsAmount(value: 400, nanos: 0))) }) case let .footer(value): @@ -145,12 +148,14 @@ private enum GlobalAutoremoveEntry: ItemListNodeEntry { }) case .priceHeader: return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.Privacy_Messages_MessagePrice, sectionId: self.section) - case let .price(value, price): - return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, minValue: 10, maxValue: 9000, value: value, price: price, sectionId: self.section, updated: { value in + 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 arguments.updateValue(.paidMessages(StarsAmount(value: value, nanos: 0))) + }, openPremiumInfo: { + arguments.openPremiumInfo() }) - case let .priceInfo(value): - return ItemListTextItem(presentationData: presentationData, text: .markdown(presentationData.strings.Privacy_Messages_MessagePriceInfo(value).string), sectionId: self.section) + case let .priceInfo(commission, value): + return ItemListTextItem(presentationData: presentationData, text: .markdown(presentationData.strings.Privacy_Messages_MessagePriceInfo("\(commission)", value).string), sectionId: self.section) case .exceptionsHeader: return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.Privacy_Messages_RemoveFeeHeader, sectionId: self.section) case let .exceptions(count): @@ -168,29 +173,32 @@ private struct IncomingMessagePrivacyScreenState: Equatable { var disableFor: [EnginePeer.Id: SelectivePrivacyPeer] } -private func incomingMessagePrivacyScreenEntries(presentationData: PresentationData, state: IncomingMessagePrivacyScreenState, isPremium: Bool, configuration: StarsSubscriptionConfiguration) -> [GlobalAutoremoveEntry] { +private func incomingMessagePrivacyScreenEntries(presentationData: PresentationData, state: IncomingMessagePrivacyScreenState, enableSetting: Bool, isPremium: Bool, configuration: StarsSubscriptionConfiguration) -> [GlobalAutoremoveEntry] { var entries: [GlobalAutoremoveEntry] = [] entries.append(.header) entries.append(.optionEverybody(value: state.updatedValue)) - entries.append(.optionPremium(value: state.updatedValue, isEnabled: isPremium)) - entries.append(.optionChargeForMessages(value: state.updatedValue, isEnabled: isPremium)) + entries.append(.optionPremium(value: state.updatedValue, isEnabled: enableSetting)) + if configuration.paidMessagesAvailable { + entries.append(.optionChargeForMessages(value: state.updatedValue, isEnabled: isPremium)) + } if case let .paidMessages(amount) = state.updatedValue { entries.append(.footer(value: state.updatedValue)) entries.append(.priceHeader) - var usdRate = 0.012 - if let usdWithdrawRate = configuration.usdWithdrawRate { - usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0 - } + let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0 + let price = "≈\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))" - entries.append(.price(value: amount.value, price: price)) - entries.append(.priceInfo(value: price)) - entries.append(.exceptionsHeader) - entries.append(.exceptions(count: state.disableFor.count)) - entries.append(.exceptionsInfo) + entries.append(.price(value: amount.value, maxValue: configuration.paidMessageMaxAmount, price: price, isEnabled: isPremium)) + entries.append(.priceInfo(commission: configuration.paidMessageCommissionPermille / 10, value: price)) + + if isPremium { + entries.append(.exceptionsHeader) + entries.append(.exceptions(count: state.disableFor.count)) + entries.append(.exceptionsInfo) + } } else { entries.append(.footer(value: state.updatedValue)) entries.append(.info) @@ -345,6 +353,17 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP }) pushControllerImpl?(controller) } + }, + openPremiumInfo: { + var replaceImpl: ((ViewController) -> Void)? + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .paidMessages, forceDark: false, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .paidMessages, forceDark: false, dismissed: nil) + replaceImpl?(controller) + }, dismissed: nil) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + pushControllerImpl?(controller) } ) @@ -375,7 +394,7 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP let title: ItemListControllerTitle = .text(presentationData.strings.Privacy_Messages_Title) - let entries: [GlobalAutoremoveEntry] = incomingMessagePrivacyScreenEntries(presentationData: presentationData, state: state, isPremium: enableSetting, configuration: configuration) + let entries: [GlobalAutoremoveEntry] = incomingMessagePrivacyScreenEntries(presentationData: presentationData, state: state, enableSetting: enableSetting, isPremium: context.isPremium, configuration: configuration) let animateChanges = false @@ -412,7 +431,12 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP controller?.push(c) } controller.attemptNavigation = { _ in - update(stateValue.with({ $0 }).updatedValue) + let updatedValue = stateValue.with({ $0 }).updatedValue + if !context.isPremium, case .paidMessages = updatedValue { + + } else { + update(updatedValue) + } return true } dismissImpl = { [weak controller] in diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index b5becc1388..f33f401371 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -437,9 +437,8 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { label = presentationData.strings.Settings_Privacy_Messages_ValueEveryone case .requirePremium: label = presentationData.strings.Settings_Privacy_Messages_ValueContactsAndPremium - case let .paidMessages(amount): - //TODO:localize - label = "\(amount.value) Stars" + case .paidMessages: + label = presentationData.strings.Settings_Privacy_Messages_ValuePaid } return ItemListDisclosureItem(presentationData: presentationData, title: presentationData.strings.Settings_Privacy_Messages, titleIcon: hasPremium ? PresentationResourcesItemList.premiumIcon(theme) : nil, label: label, sectionId: self.section, style: .blocks, action: { arguments.openMessagePrivacy() @@ -862,7 +861,11 @@ public func privacyAndSecurityController( updateHasTwoStepAuth() var setupEmailImpl: ((String?) -> Void)? - + + var reviewCallPrivacySuggestion = false + var reviewInvitePrivacySuggestion = false + var showPrivacySuggestionImpl: (() -> Void)? + let arguments = PrivacyAndSecurityControllerArguments(account: context.account, openBlockedUsers: { pushControllerImpl?(blockedPeersController(context: context, blockedPeersContext: blockedPeersContext), true) }, openLastSeenPrivacy: { @@ -907,6 +910,10 @@ public func privacyAndSecurityController( return .complete() } currentInfoDisposable.set(applySetting.start()) + + Queue.mainQueue().after(0.3) { + showPrivacySuggestionImpl?() + } } }), true) } @@ -944,6 +951,10 @@ public func privacyAndSecurityController( return .complete() } currentInfoDisposable.set(applySetting.start()) + + Queue.mainQueue().after(0.3) { + showPrivacySuggestionImpl?() + } } }), true) } @@ -1319,6 +1330,22 @@ public func privacyAndSecurityController( return state } })) + + if case .everybody = privacySettings.globalSettings.nonContactChatsPrivacy { + if case .everybody = settingValue { + + } else { + if case .enableEveryone = privacySettings.voiceCalls { + reviewCallPrivacySuggestion = true + } + if case .enableEveryone = privacySettings.groupInvitations { + reviewInvitePrivacySuggestion = true + } + Queue.mainQueue().after(0.3) { + showPrivacySuggestionImpl?() + } + } + } }), true) }) }, openGiftsPrivacy: { @@ -1442,6 +1469,50 @@ public func privacyAndSecurityController( } } + showPrivacySuggestionImpl = { + //TODO:localize + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + if reviewCallPrivacySuggestion { + reviewCallPrivacySuggestion = false + let alertController = textAlertController( + context: context, + title: "Call Settings", + text: "You've restricted who can message you, but anyone can still call you. Would you like to review these settings?", + actions: [ + TextAlertAction(type: .defaultAction, title: "Review", action: { + arguments.openVoiceCallPrivacy() + }), + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + Queue.mainQueue().after(0.3) { + showPrivacySuggestionImpl?() + } + }) + ], + actionLayout: .vertical + ) + presentControllerImpl?(alertController) + } else if reviewInvitePrivacySuggestion { + reviewInvitePrivacySuggestion = false + let alertController = textAlertController( + context: context, + title: "Invitation Settings", + text: "You've restricted who can message you, but anyone can still invite you to groups and channels. Would you like to review these settings?", + actions: [ + TextAlertAction(type: .defaultAction, title: "Review", action: { + arguments.openGroupsPrivacy() + }), + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + Queue.mainQueue().after(0.3) { + showPrivacySuggestionImpl?() + } + }) + ], + actionLayout: .vertical + ) + presentControllerImpl?(alertController) + } + } + setupEmailImpl = { emailPattern in let presentationData = context.sharedContext.currentPresentationData.with { $0 } var dismissEmailControllerImpl: (() -> Void)? diff --git a/submodules/ShareController/BUILD b/submodules/ShareController/BUILD index 6f795ea13c..e510451915 100644 --- a/submodules/ShareController/BUILD +++ b/submodules/ShareController/BUILD @@ -46,6 +46,7 @@ swift_library( "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/MessageInputPanelComponent", "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", + "//submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController", "//submodules/ChatPresentationInterfaceState", "//submodules/CheckNode", ], diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index b2d215f889..f2152f33bb 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -21,6 +21,7 @@ import AnimationCache import MultiAnimationRenderer import ObjectiveC import UndoUI +import ChatMessagePaymentAlertController private var ObjCKey_DeinitWatcher: Int? @@ -405,7 +406,7 @@ public final class ShareController: ViewController { private let fromForeignApp: Bool private let collectibleItemInfo: TelegramCollectibleItemInfo? - private let peers = Promise<([(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)], EnginePeer)>() + private let peers = Promise<([(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)], EnginePeer)>() private let peersDisposable = MetaDisposable() private let readyDisposable = MetaDisposable() private let accountActiveDisposable = MetaDisposable() @@ -664,12 +665,21 @@ public final class ShareController: ViewController { override public func loadDisplayNode() { var fromPublicChannel = false + var messageCount: Int = 1 if case let .messages(messages) = self.subject, let message = messages.first, let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { fromPublicChannel = true } else if case let .url(link) = self.subject, link.contains("t.me/nft/") { fromPublicChannel = true } + if case let .messages(messages) = self.subject { + messageCount = messages.count + } else if case let .image(images) = self.subject { + messageCount = images.count + } else if case let .fromExternal(count, _) = self.subject { + messageCount = count + } + var mediaParameters: ShareControllerSubject.MediaParameters? if case let .media(_, parameters) = self.subject { mediaParameters = parameters @@ -682,7 +692,7 @@ public final class ShareController: ViewController { return } strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, fromForeignApp: self.fromForeignApp, forceTheme: self.forceTheme, fromPublicChannel: fromPublicChannel, segmentedValues: self.segmentedValues, shareStory: self.shareStory, collectibleItemInfo: self.collectibleItemInfo) + }, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, fromForeignApp: self.fromForeignApp, forceTheme: self.forceTheme, fromPublicChannel: fromPublicChannel, segmentedValues: self.segmentedValues, shareStory: self.shareStory, collectibleItemInfo: self.collectibleItemInfo, messageCount: messageCount) self.controllerNode.completed = self.completed self.controllerNode.enqueued = self.enqueued self.controllerNode.present = { [weak self] c in @@ -1240,21 +1250,31 @@ public final class ShareController: ViewController { return self.currentContext.stateManager.postbox.combinedView( keys: peerIds.map { peerId in return PostboxViewKey.basicPeer(peerId) + } + peerIds.map { peerId in + return PostboxViewKey.cachedPeerData(peerId: peerId) } ) |> take(1) - |> map { views -> [EnginePeer.Id: EnginePeer?] in + |> map { views -> ([EnginePeer.Id: EnginePeer?], [EnginePeer.Id: StarsAmount]) in var result: [EnginePeer.Id: EnginePeer?] = [:] + var requiresStars: [EnginePeer.Id: StarsAmount] = [:] for peerId in peerIds { if let view = views.views[PostboxViewKey.basicPeer(peerId)] as? BasicPeerView, let peer = view.peer { result[peerId] = EnginePeer(peer) + if peer is TelegramUser, let cachedPeerDataView = views.views[PostboxViewKey.cachedPeerData(peerId: peerId)] as? CachedPeerDataView { + if let cachedData = cachedPeerDataView.cachedPeerData as? CachedUserData { + requiresStars[peerId] = cachedData.sendPaidMessageStars + } + } else if let channel = peer as? TelegramChannel { + requiresStars[peerId] = channel.sendPaidMessageStars + } } } - return result + return (result, requiresStars) } |> deliverOnMainQueue |> castError(ShareControllerError.self) - |> mapToSignal { [weak self] peers -> Signal in + |> mapToSignal { [weak self] peers, requiresStars -> Signal in guard let strongSelf = self else { return .complete() } @@ -1266,7 +1286,7 @@ public final class ShareController: ViewController { subject = selectedValue.subject } - func transformMessages(_ messages: [StandaloneSendEnqueueMessage], showNames: Bool, silently: Bool) -> [StandaloneSendEnqueueMessage] { + func transformMessages(_ messages: [StandaloneSendEnqueueMessage], showNames: Bool, silently: Bool, sendPaidMessageStars: StarsAmount?) -> [StandaloneSendEnqueueMessage] { return messages.map { message in var message = message if !showNames { @@ -1278,6 +1298,7 @@ public final class ShareController: ViewController { if silently { message.isSilent = true } + message.sendPaidMessageStars = sendPaidMessageStars return message } } @@ -1325,7 +1346,7 @@ public final class ShareController: ViewController { replyToMessageId: replyToMessageId )) } - messages = transformMessages(messages, showNames: showNames, silently: silently) + messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId]) shareSignals.append(standaloneSendEnqueueMessages( accountPeerId: strongSelf.currentContext.accountPeerId, postbox: strongSelf.currentContext.stateManager.postbox, @@ -1386,7 +1407,7 @@ public final class ShareController: ViewController { )), replyToMessageId: replyToMessageId )) - messages = transformMessages(messages, showNames: showNames, silently: silently) + messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId]) shareSignals.append(standaloneSendEnqueueMessages( accountPeerId: strongSelf.currentContext.accountPeerId, postbox: strongSelf.currentContext.stateManager.postbox, @@ -1451,7 +1472,7 @@ public final class ShareController: ViewController { )), replyToMessageId: replyToMessageId )) - messages = transformMessages(messages, showNames: showNames, silently: silently) + messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId]) shareSignals.append(standaloneSendEnqueueMessages( accountPeerId: strongSelf.currentContext.accountPeerId, postbox: strongSelf.currentContext.stateManager.postbox, @@ -1511,7 +1532,7 @@ public final class ShareController: ViewController { replyToMessageId: replyToMessageId )) } - messages = transformMessages(messages, showNames: showNames, silently: silently) + messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId]) shareSignals.append(standaloneSendEnqueueMessages( accountPeerId: strongSelf.currentContext.accountPeerId, postbox: strongSelf.currentContext.stateManager.postbox, @@ -1621,7 +1642,7 @@ public final class ShareController: ViewController { ), replyToMessageId: replyToMessageId )) - messages = transformMessages(messages, showNames: showNames, silently: silently) + messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId]) shareSignals.append(standaloneSendEnqueueMessages( accountPeerId: strongSelf.currentContext.accountPeerId, postbox: strongSelf.currentContext.stateManager.postbox, @@ -1680,7 +1701,7 @@ public final class ShareController: ViewController { content: .map(map: media), replyToMessageId: replyToMessageId )) - messages = transformMessages(messages, showNames: showNames, silently: silently) + messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId]) shareSignals.append(standaloneSendEnqueueMessages( accountPeerId: strongSelf.currentContext.accountPeerId, postbox: strongSelf.currentContext.stateManager.postbox, @@ -1800,7 +1821,7 @@ public final class ShareController: ViewController { replyToMessageId: replyToMessageId )) } - messagesToEnqueue = transformMessages(messagesToEnqueue, showNames: showNames, silently: silently) + messagesToEnqueue = transformMessages(messagesToEnqueue, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId]) shareSignals.append(standaloneSendEnqueueMessages( accountPeerId: strongSelf.currentContext.accountPeerId, postbox: strongSelf.currentContext.stateManager.postbox, @@ -1820,8 +1841,8 @@ public final class ShareController: ViewController { messages: messagesToEnqueue )) } - case let .fromExternal(f): - return f(peerIds, topicIds, text, strongSelf.currentContext, silently) + case let .fromExternal(_, f): + return f(peerIds, topicIds, requiresStars, text, strongSelf.currentContext, silently) |> map { state -> ShareState in switch state { case let .preparing(long): @@ -1880,12 +1901,34 @@ public final class ShareController: ViewController { guard let currentContext = self.currentContext as? ShareControllerAppAccountContext else { return .single(.done([])) } - return currentContext.context.engine.data.get(EngineDataMap( - peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)) - )) + return currentContext.stateManager.postbox.combinedView( + keys: peerIds.map { peerId in + return PostboxViewKey.basicPeer(peerId) + } + peerIds.map { peerId in + return PostboxViewKey.cachedPeerData(peerId: peerId) + } + ) + |> take(1) + |> map { views -> ([EnginePeer.Id: EnginePeer?], [EnginePeer.Id: StarsAmount]) in + var result: [EnginePeer.Id: EnginePeer?] = [:] + var requiresStars: [EnginePeer.Id: StarsAmount] = [:] + for peerId in peerIds { + if let view = views.views[PostboxViewKey.basicPeer(peerId)] as? BasicPeerView, let peer = view.peer { + result[peerId] = EnginePeer(peer) + if peer is TelegramUser, let cachedPeerDataView = views.views[PostboxViewKey.cachedPeerData(peerId: peerId)] as? CachedPeerDataView { + if let cachedData = cachedPeerDataView.cachedPeerData as? CachedUserData { + requiresStars[peerId] = cachedData.sendPaidMessageStars + } + } else if let channel = peer as? TelegramChannel { + requiresStars[peerId] = channel.sendPaidMessageStars + } + } + } + return (result, requiresStars) + } |> deliverOnMainQueue |> castError(ShareControllerError.self) - |> mapToSignal { [weak self] peers -> Signal in + |> mapToSignal { [weak self] peers, requiresStars -> Signal in guard let strongSelf = self, let currentContext = strongSelf.currentContext as? ShareControllerAppAccountContext else { return .complete() } @@ -1897,7 +1940,7 @@ public final class ShareController: ViewController { subject = selectedValue.subject } - func transformMessages(_ messages: [EnqueueMessage], showNames: Bool, silently: Bool) -> [EnqueueMessage] { + func transformMessages(_ messages: [EnqueueMessage], showNames: Bool, silently: Bool, sendPaidMessageStars: StarsAmount?) -> [EnqueueMessage] { return messages.map { message in return message.withUpdatedAttributes({ attributes in var attributes = attributes @@ -1907,6 +1950,9 @@ public final class ShareController: ViewController { if silently { attributes.append(NotificationInfoMessageAttribute(flags: .muted)) } + if let sendPaidMessageStars { + attributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: false)) + } return attributes }) } @@ -1949,7 +1995,7 @@ public final class ShareController: ViewController { } else { messages.append(.message(text: url, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } - messages = transformMessages(messages, showNames: showNames, silently: silently) + messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId]) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } case let .text(string): @@ -1983,7 +2029,7 @@ public final class ShareController: ViewController { messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } messages.append(.message(text: string, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) - messages = transformMessages(messages, showNames: showNames, silently: silently) + messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId]) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } case let .quote(string, url): @@ -2020,7 +2066,7 @@ public final class ShareController: ViewController { attributedText.append(NSAttributedString(string: "\n\n\(url)")) let entities = generateChatInputTextEntities(attributedText) messages.append(.message(text: attributedText.string, attributes: [TextEntitiesMessageAttribute(entities: entities)], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) - messages = transformMessages(messages, showNames: showNames, silently: silently) + messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId]) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } case let .image(representations): @@ -2051,7 +2097,7 @@ public final class ShareController: ViewController { var messages: [EnqueueMessage] = [] messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: representations.map({ $0.representation }), immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])), threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) - messages = transformMessages(messages, showNames: showNames, silently: silently) + messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId]) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } case let .media(mediaReference, mediaParameters): @@ -2145,7 +2191,7 @@ public final class ShareController: ViewController { } else { messages.append(.message(text: sendTextAsCaption ? text : "", attributes: attributes, inlineStickers: [:], mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } - messages = transformMessages(messages, showNames: showNames, silently: silently) + messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId]) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } case let .mapMedia(media): @@ -2179,7 +2225,7 @@ public final class ShareController: ViewController { messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) - messages = transformMessages(messages, showNames: showNames, silently: silently) + messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId]) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } case let .messages(messages): @@ -2274,11 +2320,11 @@ public final class ShareController: ViewController { correlationIds.append(correlationId) messagesToEnqueue.append(.forward(source: message.id, threadId: threadId, grouping: .auto, attributes: [], correlationId: correlationId)) } - messagesToEnqueue = transformMessages(messagesToEnqueue, showNames: showNames, silently: silently) + messagesToEnqueue = transformMessages(messagesToEnqueue, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId]) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messagesToEnqueue)) } - case let .fromExternal(f): - return f(peerIds, topicIds, text, currentContext, silently) + case let .fromExternal(_, f): + return f(peerIds, topicIds, requiresStars, text, currentContext, silently) |> map { state -> ShareState in switch state { case let .preparing(long): @@ -2458,7 +2504,7 @@ public final class ShareController: ViewController { peer, tailChatList |> take(1) ) - |> mapToSignal { maybeAccountPeer, view -> Signal<([(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)], EnginePeer), NoError> in + |> mapToSignal { maybeAccountPeer, view -> Signal<([(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)], EnginePeer), NoError> in let accountPeer = maybeAccountPeer! var peers: [EngineRenderedPeer] = [] @@ -2468,7 +2514,7 @@ public final class ShareController: ViewController { case let .MessageEntry(entryData): if let peer = entryData.renderedPeer.peers[entryData.renderedPeer.peerId], peer.id != accountPeer.id, canSendMessagesToPeer(peer) { peers.append(EngineRenderedPeer(entryData.renderedPeer)) - if let user = peer as? TelegramUser, user.flags.contains(.requirePremium) { + if let user = peer as? TelegramUser, user.flags.contains(.requirePremium) || user.flags.contains(.requireStars) { possiblePremiumRequiredPeers.insert(user.id) } } @@ -2487,7 +2533,7 @@ public final class ShareController: ViewController { } return account.stateManager.postbox.combinedView(keys: keys) - |> map { views -> ([EnginePeer.Id: EnginePeer.Presence?], [EnginePeer.Id: Bool]) in + |> map { views -> ([EnginePeer.Id: EnginePeer.Presence?], [EnginePeer.Id: Bool], [EnginePeer.Id: Int64]) in var result: [EnginePeer.Id: EnginePeer.Presence?] = [:] if let view = views.views[peerPresencesKey] as? PeerPresencesView { result = view.presences.mapValues { value -> EnginePeer.Presence? in @@ -2495,19 +2541,21 @@ public final class ShareController: ViewController { } } var requiresPremiumForMessaging: [EnginePeer.Id: Bool] = [:] + var requiresStars: [EnginePeer.Id: Int64] = [:] for id in possiblePremiumRequiredPeers { if let view = views.views[.cachedPeerData(peerId: id)] as? CachedPeerDataView, let data = view.cachedPeerData as? CachedUserData { requiresPremiumForMessaging[id] = data.flags.contains(.premiumRequired) + requiresStars[id] = data.sendPaidMessageStars?.value } else { requiresPremiumForMessaging[id] = false } } - return (result, requiresPremiumForMessaging) + return (result, requiresPremiumForMessaging, requiresStars) } - |> map { presenceMap, requiresPremiumForMessaging -> ([(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)], EnginePeer) in - var resultPeers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)] = [] + |> map { presenceMap, requiresPremiumForMessaging, requiresStars -> ([(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)], EnginePeer) in + var resultPeers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)] = [] for peer in peers { - resultPeers.append((peer, presenceMap[peer.peerId].flatMap { $0 }, requiresPremiumForMessaging[peer.peerId] ?? false)) + resultPeers.append((peer, presenceMap[peer.peerId].flatMap { $0 }, requiresPremiumForMessaging[peer.peerId] ?? false, requiresStars[peer.peerId])) } return (resultPeers, accountPeer) } diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index cb255ae56a..3c71ca02d1 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -15,6 +15,7 @@ import TelegramStringFormatting import BundleIconComponent import LottieComponent import CheckNode +import ChatMessagePaymentAlertController enum ShareState { case preparing(Bool) @@ -327,6 +328,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate private let segmentedValues: [ShareControllerSegmentedValue]? private let collectibleItemInfo: TelegramCollectibleItemInfo? private let mediaParameters: ShareControllerSubject.MediaParameters? + private let messageCount: Int var selectedSegmentedIndex: Int = 0 @@ -387,7 +389,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate private let showNames = ValuePromise(true) - init(controller: ShareController, environment: ShareControllerEnvironment, presentationData: PresentationData, presetText: String?, defaultAction: ShareControllerAction?, mediaParameters: ShareControllerSubject.MediaParameters?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forceTheme: PresentationTheme?, fromPublicChannel: Bool, segmentedValues: [ShareControllerSegmentedValue]?, shareStory: (() -> Void)?, collectibleItemInfo: TelegramCollectibleItemInfo?) { + init(controller: ShareController, environment: ShareControllerEnvironment, presentationData: PresentationData, presetText: String?, defaultAction: ShareControllerAction?, mediaParameters: ShareControllerSubject.MediaParameters?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forceTheme: PresentationTheme?, fromPublicChannel: Bool, segmentedValues: [ShareControllerSegmentedValue]?, shareStory: (() -> Void)?, collectibleItemInfo: TelegramCollectibleItemInfo?, messageCount: Int) { self.controller = controller self.environment = environment self.presentationData = presentationData @@ -401,6 +403,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate self.segmentedValues = segmentedValues self.collectibleItemInfo = collectibleItemInfo self.mediaParameters = mediaParameters + self.messageCount = messageCount self.presetText = presetText @@ -1260,7 +1263,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate }) } } - + func send(peerId: PeerId? = nil, showNames: Bool = true, silently: Bool = false) { let peerIds: [PeerId] if let peerId = peerId { @@ -1273,19 +1276,29 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate let _ = (context.stateManager.postbox.combinedView( keys: peerIds.map { peerId in return PostboxViewKey.basicPeer(peerId) + } + peerIds.map { peerId in + return PostboxViewKey.cachedPeerData(peerId: peerId) } ) |> take(1) - |> map { views -> [EnginePeer.Id: EnginePeer?] in + |> map { views -> ([EnginePeer.Id: EnginePeer?], [EnginePeer.Id: Int64]) in var result: [EnginePeer.Id: EnginePeer?] = [:] + var requiresStars: [EnginePeer.Id: Int64] = [:] for peerId in peerIds { if let view = views.views[PostboxViewKey.basicPeer(peerId)] as? BasicPeerView, let peer = view.peer { result[peerId] = EnginePeer(peer) + if peer is TelegramUser, let cachedPeerDataView = views.views[PostboxViewKey.cachedPeerData(peerId: peerId)] as? CachedPeerDataView { + if let cachedData = cachedPeerDataView.cachedPeerData as? CachedUserData { + requiresStars[peerId] = cachedData.sendPaidMessageStars?.value + } + } else if let channel = peer as? TelegramChannel { + requiresStars[peerId] = channel.sendPaidMessageStars?.value + } } } - return result + return (result, requiresStars) } - |> deliverOnMainQueue).start(next: { [weak self] peers in + |> deliverOnMainQueue).start(next: { [weak self] peers, requiresStars in guard let self else { return } @@ -1300,14 +1313,49 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate if !tryShare(self.inputFieldNode.text, mappedPeers) { return } + + self.presentPaidMessageAlertIfNeeded(peers: mappedPeers, requiresStars: requiresStars, completion: { [weak self] in + self?.commitSend(peerId: peerId, showNames: showNames, silently: silently) + }) - self.commitSend(peerId: peerId, showNames: showNames, silently: silently) }) } else { self.commitSend(peerId: peerId, showNames: showNames, silently: silently) } } + private func presentPaidMessageAlertIfNeeded(peers: [EnginePeer], requiresStars: [EnginePeer.Id: Int64], completion: @escaping () -> Void) { + var count: Int32 = Int32(self.messageCount) + if !self.inputFieldNode.text.isEmpty { + count += 1 + } + var totalAmount: StarsAmount = .zero + for peer in peers { + if let stars = requiresStars[peer.id] { + totalAmount = totalAmount + StarsAmount(value: stars, nanos: 0) + } + } + if totalAmount.value > 0 { + let controller = chatMessagePaymentAlertController( + context: nil, + presentationData: self.presentationData, + updatedPresentationData: nil, + peers: peers, + count: count, + amount: totalAmount, + totalAmount: totalAmount, + hasCheck: false, + navigationController: nil, + completion: { _ in + completion() + } + ) + self.present?(controller) + } else { + completion() + } + } + private func commitSend(peerId: PeerId?, showNames: Bool, silently: Bool) { if !self.inputFieldNode.text.isEmpty { for peer in self.controllerInteraction!.selectedPeers { @@ -1522,7 +1570,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate } } - func updatePeers(context: ShareControllerAccountContext, switchableAccounts: [ShareControllerSwitchableAccount], peers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)], accountPeer: EnginePeer, defaultAction: ShareControllerAction?) { + func updatePeers(context: ShareControllerAccountContext, switchableAccounts: [ShareControllerSwitchableAccount], peers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)], accountPeer: EnginePeer, defaultAction: ShareControllerAction?) { self.context = context if let peersContentNode = self.peersContentNode, peersContentNode.accountPeer.id == accountPeer.id { diff --git a/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift b/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift index f70a91a523..e345229d48 100644 --- a/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift +++ b/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift @@ -96,11 +96,11 @@ final class ShareControllerGridSectionNode: ASDisplayNode { final class ShareControllerPeerGridItem: GridItem { enum ShareItem: Equatable { - case peer(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, topicId: Int64?, threadData: MessageHistoryThreadData?, requiresPremiumForMessaging: Bool) + case peer(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, topicId: Int64?, threadData: MessageHistoryThreadData?, requiresPremiumForMessaging: Bool, requiresStars: Int64?) case story(isMessage: Bool) var peerId: EnginePeer.Id? { - if case let .peer(peer, _, _, _, _) = self { + if case let .peer(peer, _, _, _, _, _) = self { return peer.peerId } else { return nil @@ -162,7 +162,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode { private var absoluteLocation: (CGRect, CGSize)? var peerId: EnginePeer.Id? { - if let item = self.currentState?.item, case let .peer(peer, _, _, _, _) = item { + if let item = self.currentState?.item, case let .peer(peer, _, _, _, _, _) = item { return peer.peerId } else { return nil @@ -177,7 +177,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode { self.peerNode.toggleSelection = { [weak self] isDisabled in if let strongSelf = self { if let (_, _, _, _, maybeItem, search) = strongSelf.currentState, let item = maybeItem { - if case let .peer(peer, _, _, _, _) = item, let _ = peer.peers[peer.peerId] { + if case let .peer(peer, _, _, _, _, _) = item, let _ = peer.peers[peer.peerId] { if isDisabled { strongSelf.controllerInteraction?.disabledPeerSelected(peer) } else { @@ -213,7 +213,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode { var effectivePresence: EnginePeer.Presence? let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) self.peerNode.theme = itemTheme - if let item, case let .peer(renderedPeer, presence, _, threadData, requiresPremiumForMessaging) = item, let peer = renderedPeer.peer { + if let item, case let .peer(renderedPeer, presence, _, threadData, requiresPremiumForMessaging, requiresStars) = item, let peer = renderedPeer.peer { effectivePresence = presence var isOnline = false var isSupport = false @@ -243,6 +243,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode { strings: strings, peer: renderedPeer, requiresPremiumForMessaging: requiresPremiumForMessaging, + requiresStars: requiresStars, customTitle: threadData?.info.title, iconId: threadData?.info.icon, iconColor: threadData?.info.iconColor ?? 0, @@ -302,7 +303,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode { func updateSelection(animated: Bool) { var selected = false if let controllerInteraction = self.controllerInteraction, let (_, _, _, _, maybeItem, _) = self.currentState, let item = maybeItem { - if case let .peer(peer, _, _, _, _) = item { + if case let .peer(peer, _, _, _, _, _) = item { selected = controllerInteraction.selectedPeerIds.contains(peer.peerId) } } diff --git a/submodules/ShareController/Sources/SharePeersContainerNode.swift b/submodules/ShareController/Sources/SharePeersContainerNode.swift index 3b025e3a6a..6680872509 100644 --- a/submodules/ShareController/Sources/SharePeersContainerNode.swift +++ b/submodules/ShareController/Sources/SharePeersContainerNode.swift @@ -43,7 +43,7 @@ private struct SharePeerEntry: Comparable, Identifiable { var stableId: Int64 { switch self.item { - case let .peer(peer, _, _, _, _): + case let .peer(peer, _, _, _, _, _): return peer.peerId.toInt64() case .story: return 0 @@ -137,7 +137,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { private var validLayout: (CGSize, CGFloat)? private var overrideGridOffsetTransition: ContainedViewLayoutTransition? - let peersValue = Promise<[(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)]>() + let peersValue = Promise<[(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)]>() private var _tick: Int = 0 { didSet { @@ -146,7 +146,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { } private let tick = ValuePromise(0) - init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, switchableAccounts: [ShareControllerSwitchableAccount], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)], accountPeer: EnginePeer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void, debugAction: @escaping () -> Void, extendedInitialReveal: Bool, segmentedValues: [ShareControllerSegmentedValue]?, fromPublicChannel: Bool) { + init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, switchableAccounts: [ShareControllerSwitchableAccount], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)], accountPeer: EnginePeer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void, debugAction: @escaping () -> Void, extendedInitialReveal: Bool, segmentedValues: [ShareControllerSegmentedValue]?, fromPublicChannel: Bool) { self.environment = environment self.context = context self.theme = theme @@ -176,22 +176,22 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { } var existingPeerIds: Set = Set() - entries.append(SharePeerEntry(index: index, item: .peer(peer: EngineRenderedPeer(peer: accountPeer), presence: nil, topicId: nil, threadData: nil, requiresPremiumForMessaging: false), theme: theme, strings: strings)) + entries.append(SharePeerEntry(index: index, item: .peer(peer: EngineRenderedPeer(peer: accountPeer), presence: nil, topicId: nil, threadData: nil, requiresPremiumForMessaging: false, requiresStars: nil), theme: theme, strings: strings)) existingPeerIds.insert(accountPeer.id) index += 1 for (peer, requiresPremiumForMessaging) in foundPeers.reversed() { if !existingPeerIds.contains(peer.peerId) { - entries.append(SharePeerEntry(index: index, item: .peer(peer: peer, presence: nil, topicId: nil, threadData: nil, requiresPremiumForMessaging: requiresPremiumForMessaging), theme: theme, strings: strings)) + entries.append(SharePeerEntry(index: index, item: .peer(peer: peer, presence: nil, topicId: nil, threadData: nil, requiresPremiumForMessaging: requiresPremiumForMessaging, requiresStars: nil), theme: theme, strings: strings)) existingPeerIds.insert(peer.peerId) index += 1 } } - for (peer, presence, requiresPremiumForMessaging) in initialPeers { + for (peer, presence, requiresPremiumForMessaging, requiresStars) in initialPeers { if !existingPeerIds.contains(peer.peerId) { let thread = controllerInteraction?.selectedTopics[peer.peerId] - entries.append(SharePeerEntry(index: index, item: .peer(peer: peer, presence: presence, topicId: thread?.0, threadData: thread?.1, requiresPremiumForMessaging: requiresPremiumForMessaging), theme: theme, strings: strings)) + entries.append(SharePeerEntry(index: index, item: .peer(peer: peer, presence: presence, topicId: thread?.0, threadData: thread?.1, requiresPremiumForMessaging: requiresPremiumForMessaging, requiresStars: requiresStars), theme: theme, strings: strings)) existingPeerIds.insert(peer.peerId) index += 1 } diff --git a/submodules/ShareController/Sources/ShareSearchContainerNode.swift b/submodules/ShareController/Sources/ShareSearchContainerNode.swift index 0048043963..ee3b966d15 100644 --- a/submodules/ShareController/Sources/ShareSearchContainerNode.swift +++ b/submodules/ShareController/Sources/ShareSearchContainerNode.swift @@ -36,13 +36,13 @@ private enum ShareSearchRecentEntryStableId: Hashable { private enum ShareSearchRecentEntry: Comparable, Identifiable { case topPeers(PresentationTheme, PresentationStrings) - case peer(index: Int, theme: PresentationTheme, peer: EnginePeer, associatedPeer: EnginePeer?, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, strings: PresentationStrings) + case peer(index: Int, theme: PresentationTheme, peer: EnginePeer, associatedPeer: EnginePeer?, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?, strings: PresentationStrings) var stableId: ShareSearchRecentEntryStableId { switch self { case .topPeers: return .topPeers - case let .peer(_, _, peer, _, _, _, _): + case let .peer(_, _, peer, _, _, _, _, _): return .peerId(peer.id) } } @@ -61,8 +61,8 @@ private enum ShareSearchRecentEntry: Comparable, Identifiable { } else { return false } - case let .peer(lhsIndex, lhsTheme, lhsPeer, lhsAssociatedPeer, lhsPresence, lhsRequiresPremiumForMessaging, lhsStrings): - if case let .peer(rhsIndex, rhsTheme, rhsPeer, rhsAssociatedPeer, rhsPresence, rhsRequiresPremiumForMessaging, rhsStrings) = rhs, lhsPeer == rhsPeer && lhsAssociatedPeer == rhsAssociatedPeer && lhsIndex == rhsIndex && lhsStrings === rhsStrings && lhsTheme === rhsTheme && lhsPresence == rhsPresence && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging { + case let .peer(lhsIndex, lhsTheme, lhsPeer, lhsAssociatedPeer, lhsPresence, lhsRequiresPremiumForMessaging, lhsRequiresStars, lhsStrings): + if case let .peer(rhsIndex, rhsTheme, rhsPeer, rhsAssociatedPeer, rhsPresence, rhsRequiresPremiumForMessaging, rhsRequiresStars, rhsStrings) = rhs, lhsPeer == rhsPeer && lhsAssociatedPeer == rhsAssociatedPeer && lhsIndex == rhsIndex && lhsStrings === rhsStrings && lhsTheme === rhsTheme && lhsPresence == rhsPresence && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging && lhsRequiresStars == rhsRequiresStars { return true } else { return false @@ -74,11 +74,11 @@ private enum ShareSearchRecentEntry: Comparable, Identifiable { switch lhs { case .topPeers: return true - case let .peer(lhsIndex, _, _, _, _, _, _): + case let .peer(lhsIndex, _, _, _, _, _, _, _): switch rhs { case .topPeers: return false - case let .peer(rhsIndex, _, _, _, _, _, _): + case let .peer(rhsIndex, _, _, _, _, _, _, _): return lhsIndex <= rhsIndex } } @@ -88,13 +88,13 @@ private enum ShareSearchRecentEntry: Comparable, Identifiable { switch self { case let .topPeers(theme, strings): return ShareControllerRecentPeersGridItem(environment: environment, context: context, theme: theme, strings: strings, controllerInteraction: interfaceInteraction) - case let .peer(_, theme, peer, associatedPeer, presence, requiresPremiumForMessaging, strings): + case let .peer(_, theme, peer, associatedPeer, presence, requiresPremiumForMessaging, requiresStars, strings): var peers: [EnginePeer.Id: EnginePeer] = [peer.id: peer] if let associatedPeer = associatedPeer { peers[associatedPeer.id] = associatedPeer } let peer = EngineRenderedPeer(peerId: peer.id, peers: peers, associatedMedia: [:]) - return ShareControllerPeerGridItem(environment: environment, context: context, theme: theme, strings: strings, item: .peer(peer: peer, presence: presence, topicId: nil, threadData: nil, requiresPremiumForMessaging: requiresPremiumForMessaging), controllerInteraction: interfaceInteraction, sectionTitle: strings.DialogList_SearchSectionRecent, search: true) + return ShareControllerPeerGridItem(environment: environment, context: context, theme: theme, strings: strings, item: .peer(peer: peer, presence: presence, topicId: nil, threadData: nil, requiresPremiumForMessaging: requiresPremiumForMessaging, requiresStars: requiresStars), controllerInteraction: interfaceInteraction, sectionTitle: strings.DialogList_SearchSectionRecent, search: true) } } } @@ -104,6 +104,7 @@ private struct ShareSearchPeerEntry: Comparable, Identifiable { let peer: EngineRenderedPeer? let presence: EnginePeer.Presence? let requiresPremiumForMessaging: Bool + let requiresStars: Int64? let theme: PresentationTheme let strings: PresentationStrings let isGlobal: Bool @@ -129,6 +130,9 @@ private struct ShareSearchPeerEntry: Comparable, Identifiable { if lhs.requiresPremiumForMessaging != rhs.requiresPremiumForMessaging { return false } + if lhs.requiresStars != rhs.requiresStars { + return false + } if lhs.theme !== rhs.theme { return false } @@ -149,7 +153,7 @@ private struct ShareSearchPeerEntry: Comparable, Identifiable { } else { sectionTitle = nil } - return ShareControllerPeerGridItem(environment: environment, context: context, theme: self.theme, strings: self.strings, item: self.peer.flatMap({ .peer(peer: $0, presence: self.presence, topicId: nil, threadData: nil, requiresPremiumForMessaging: self.requiresPremiumForMessaging) }), controllerInteraction: interfaceInteraction, sectionTitle: sectionTitle, search: true) + return ShareControllerPeerGridItem(environment: environment, context: context, theme: self.theme, strings: self.strings, item: self.peer.flatMap({ .peer(peer: $0, presence: self.presence, topicId: nil, threadData: nil, requiresPremiumForMessaging: self.requiresPremiumForMessaging, requiresStars: self.requiresStars) }), controllerInteraction: interfaceInteraction, sectionTitle: sectionTitle, search: true) } } @@ -361,7 +365,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { if strings.DialogList_SavedMessages.lowercased().hasPrefix(lowercasedQuery) || "saved messages".hasPrefix(lowercasedQuery) { if !existingPeerIds.contains(accountPeer.id) { existingPeerIds.insert(accountPeer.id) - entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(accountPeer)), presence: nil, requiresPremiumForMessaging: false, theme: theme, strings: strings, isGlobal: false)) + entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(accountPeer)), presence: nil, requiresPremiumForMessaging: false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false)) index += 1 } } @@ -370,7 +374,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != accountPeer.id { if !existingPeerIds.contains(renderedPeer.peerId) && canSendMessagesToPeer(peer) { existingPeerIds.insert(renderedPeer.peerId) - entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(renderedPeer), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, theme: theme, strings: strings, isGlobal: false)) + entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(renderedPeer), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false)) index += 1 } } @@ -380,7 +384,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { if foundRemotePeers.2 { isPlaceholder = true for _ in 0 ..< 4 { - entries.append(ShareSearchPeerEntry(index: index, peer: nil, presence: nil, requiresPremiumForMessaging: false, theme: theme, strings: strings, isGlobal: false)) + entries.append(ShareSearchPeerEntry(index: index, peer: nil, presence: nil, requiresPremiumForMessaging: false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false)) index += 1 } } else { @@ -388,7 +392,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { let peer = foundPeer.peer if !existingPeerIds.contains(peer.id) && canSendMessagesToPeer(peer) { existingPeerIds.insert(peer.id) - entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(foundPeer.peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, theme: theme, strings: strings, isGlobal: false)) + entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(foundPeer.peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false)) index += 1 } } @@ -397,7 +401,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { let peer = foundPeer.peer if !existingPeerIds.contains(peer.id) && canSendMessagesToPeer(peer) { existingPeerIds.insert(peer.id) - entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, theme: theme, strings: strings, isGlobal: true)) + entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: true)) index += 1 } } @@ -462,7 +466,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { var index = 0 for (peer, requiresPremiumForMessaging) in recentPeerList { if let mainPeer = peer.peers[peer.peerId], canSendMessagesToPeer(mainPeer._asPeer()) { - recentItemList.append(.peer(index: index, theme: theme, peer: mainPeer, associatedPeer: mainPeer._asPeer().associatedPeerId.flatMap { peer.peers[$0] }, presence: nil, requiresPremiumForMessaging: requiresPremiumForMessaging, strings: strings)) + recentItemList.append(.peer(index: index, theme: theme, peer: mainPeer, associatedPeer: mainPeer._asPeer().associatedPeerId.flatMap { peer.peers[$0] }, presence: nil, requiresPremiumForMessaging: requiresPremiumForMessaging, requiresStars: nil, strings: strings)) index += 1 } } @@ -570,7 +574,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { switch $0 { case .topPeers: return false - case let .peer(_, _, peer, _, _, _, _): + case let .peer(_, _, peer, _, _, _, _, _): return peer.id == ensurePeerVisibleOnLayout } }) { diff --git a/submodules/ShareItems/Sources/ShareItems.swift b/submodules/ShareItems/Sources/ShareItems.swift index 84c74e819c..d063a8e0cf 100644 --- a/submodules/ShareItems/Sources/ShareItems.swift +++ b/submodules/ShareItems/Sources/ShareItems.swift @@ -409,7 +409,7 @@ public func preparedShareItems(postbox: Postbox, network: Network, to peerId: Pe }) } -public func sentShareItems(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, auxiliaryMethods: AccountAuxiliaryMethods, to peerIds: [PeerId], threadIds: [PeerId: Int64], items: [PreparedShareItemContent], silently: Bool, additionalText: String) -> Signal { +public func sentShareItems(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, auxiliaryMethods: AccountAuxiliaryMethods, to peerIds: [PeerId], threadIds: [PeerId: Int64], requireStars: [PeerId: StarsAmount], items: [PreparedShareItemContent], silently: Bool, additionalText: String) -> Signal { var messages: [StandaloneSendEnqueueMessage] = [] var groupingKey: Int64? var mediaTypes: (photo: Int, video: Int, music: Int, other: Int) = (0, 0, 0, 0) @@ -498,6 +498,16 @@ public func sentShareItems(accountPeerId: PeerId, postbox: Postbox, network: Net var peerSignals: Signal = .single(0.0) for peerId in peerIds { + var peerMessages = messages + if let amount = requireStars[peerId] { + var updatedMessages: [StandaloneSendEnqueueMessage] = [] + for message in peerMessages { + var message = message + message.sendPaidMessageStars = amount + updatedMessages.append(message) + } + peerMessages = updatedMessages + } peerSignals = peerSignals |> then(standaloneSendEnqueueMessages( accountPeerId: accountPeerId, postbox: postbox, @@ -506,7 +516,7 @@ public func sentShareItems(accountPeerId: PeerId, postbox: Postbox, network: Net auxiliaryMethods: auxiliaryMethods, peerId: peerId, threadId: threadIds[peerId], - messages: messages + messages: peerMessages ) |> mapToSignal { status -> Signal in switch status { diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 112dc6f095..0a84a4cb93 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -1182,7 +1182,7 @@ private enum StatsEntry: ItemListNodeEntry { detailText = stringForMediumCompactDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) } - let label = tonAmountAttributedString(formatTonAmountText(transaction.amount, dateTimeFormat: presentationData.dateTimeFormat, showPlus: true), integralFont: font, fractionalFont: smallLabelFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + let label = tonAmountAttributedString(formatTonAmountText(transaction.amount, dateTimeFormat: presentationData.dateTimeFormat, showPlus: true), integralFont: font, fractionalFont: smallLabelFont, color: labelColor, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator).mutableCopy() as! NSMutableAttributedString label.insert(NSAttributedString(string: " $ ", font: font, textColor: labelColor), at: 1) if let range = label.string.range(of: "$"), let icon = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonMedium"), color: labelColor) { diff --git a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift index 9164414cbb..b85bbdeb10 100644 --- a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift +++ b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift @@ -177,7 +177,7 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode { var isStars = false if let stats = item.stats as? RevenueStats { let cryptoValue = formatTonAmountText(stats.balances.availableBalance, dateTimeFormat: item.presentationData.dateTimeFormat) - amountString = tonAmountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor) + amountString = tonAmountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) value = stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))" } else if let stats = item.stats as? StarsRevenueStats { amountString = NSAttributedString(string: presentationStringsFormattedNumber(stats.balances.availableBalance, item.presentationData.dateTimeFormat.groupingSeparator), font: integralFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) diff --git a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift index 65d206204e..4fb0c2ff4c 100644 --- a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift +++ b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift @@ -201,7 +201,7 @@ private final class ValueItemNode: ASDisplayNode { let valueString: NSAttributedString if case .ton = mode { - valueString = tonAmountAttributedString(value, integralFont: valueFont, fractionalFont: smallValueFont, color: valueColor) + valueString = tonAmountAttributedString(value, integralFont: valueFont, fractionalFont: smallValueFont, color: valueColor, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) } else { valueString = NSAttributedString(string: value, font: valueFont, textColor: valueColor) } diff --git a/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift b/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift index 61491adb44..1d7edc0e22 100644 --- a/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift +++ b/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift @@ -139,7 +139,7 @@ private final class SheetContent: CombinedComponent { switch component.transaction { case let .proceeds(amount, fromDate, toDate): labelColor = theme.list.itemDisclosureActions.constructive.fillColor - amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor, decimalSeparator: dateTimeFormat.decimalSeparator).mutableCopy() as! NSMutableAttributedString dateString = "\(stringForMediumCompactDate(timestamp: fromDate, strings: strings, dateTimeFormat: dateTimeFormat)) – \(stringForMediumCompactDate(timestamp: toDate, strings: strings, dateTimeFormat: dateTimeFormat))" titleString = strings.Monetization_TransactionInfo_Proceeds buttonTitle = strings.Common_OK @@ -147,7 +147,7 @@ private final class SheetContent: CombinedComponent { showPeer = true case let .withdrawal(status, amount, date, provider, _, transactionUrl): labelColor = theme.list.itemDestructiveColor - amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor, decimalSeparator: dateTimeFormat.decimalSeparator).mutableCopy() as! NSMutableAttributedString dateString = stringForFullDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat) switch status { @@ -166,7 +166,7 @@ private final class SheetContent: CombinedComponent { case let .refund(amount, date, _): labelColor = theme.list.itemDisclosureActions.constructive.fillColor titleString = strings.Monetization_TransactionInfo_Refund - amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor, decimalSeparator: dateTimeFormat.decimalSeparator).mutableCopy() as! NSMutableAttributedString dateString = stringForFullDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat) buttonTitle = strings.Common_OK explorerUrl = nil diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 26afc9bdca..42feaf44c6 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -1390,7 +1390,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1314881805] = { return Api.payments.PaymentResult.parse_paymentResult($0) } dict[-666824391] = { return Api.payments.PaymentResult.parse_paymentVerificationNeeded($0) } dict[-74456004] = { return Api.payments.SavedInfo.parse_savedInfo($0) } - dict[-1779201615] = { return Api.payments.SavedStarGifts.parse_savedStarGifts($0) } + dict[-418915641] = { return Api.payments.SavedStarGifts.parse_savedStarGifts($0) } dict[377215243] = { return Api.payments.StarGiftUpgradePreview.parse_starGiftUpgradePreview($0) } dict[-2069218660] = { return Api.payments.StarGiftWithdrawalUrl.parse_starGiftWithdrawalUrl($0) } dict[-1877571094] = { return Api.payments.StarGifts.parse_starGifts($0) } diff --git a/submodules/TelegramApi/Sources/Api35.swift b/submodules/TelegramApi/Sources/Api35.swift index c8dbfa6fa7..6c7f89773e 100644 --- a/submodules/TelegramApi/Sources/Api35.swift +++ b/submodules/TelegramApi/Sources/Api35.swift @@ -1,16 +1,21 @@ public extension Api.payments { enum SavedStarGifts: TypeConstructorDescription { - case savedStarGifts(flags: Int32, count: Int32, chatNotificationsEnabled: Api.Bool?, gifts: [Api.SavedStarGift], nextOffset: String?, chats: [Api.Chat], users: [Api.User]) + case savedStarGifts(flags: Int32, count: Int32, chatNotificationsEnabled: Api.Bool?, pinnedToTop: [Int64]?, gifts: [Api.SavedStarGift], nextOffset: String?, chats: [Api.Chat], users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .savedStarGifts(let flags, let count, let chatNotificationsEnabled, let gifts, let nextOffset, let chats, let users): + case .savedStarGifts(let flags, let count, let chatNotificationsEnabled, let pinnedToTop, let gifts, let nextOffset, let chats, let users): if boxed { - buffer.appendInt32(-1779201615) + buffer.appendInt32(-418915641) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(count, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 1) != 0 {chatNotificationsEnabled!.serialize(buffer, true)} + if Int(flags) & Int(1 << 2) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(pinnedToTop!.count)) + for item in pinnedToTop! { + serializeInt64(item, buffer: buffer, boxed: false) + }} buffer.appendInt32(481674261) buffer.appendInt32(Int32(gifts.count)) for item in gifts { @@ -33,8 +38,8 @@ public extension Api.payments { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .savedStarGifts(let flags, let count, let chatNotificationsEnabled, let gifts, let nextOffset, let chats, let users): - return ("savedStarGifts", [("flags", flags as Any), ("count", count as Any), ("chatNotificationsEnabled", chatNotificationsEnabled as Any), ("gifts", gifts as Any), ("nextOffset", nextOffset as Any), ("chats", chats as Any), ("users", users as Any)]) + case .savedStarGifts(let flags, let count, let chatNotificationsEnabled, let pinnedToTop, let gifts, let nextOffset, let chats, let users): + return ("savedStarGifts", [("flags", flags as Any), ("count", count as Any), ("chatNotificationsEnabled", chatNotificationsEnabled as Any), ("pinnedToTop", pinnedToTop as Any), ("gifts", gifts as Any), ("nextOffset", nextOffset as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -47,29 +52,34 @@ public extension Api.payments { if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.Bool } } - var _4: [Api.SavedStarGift]? + var _4: [Int64]? + if Int(_1!) & Int(1 << 2) != 0 {if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } } + var _5: [Api.SavedStarGift]? if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SavedStarGift.self) + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SavedStarGift.self) } - var _5: String? - if Int(_1!) & Int(1 << 0) != 0 {_5 = parseString(reader) } - var _6: [Api.Chat]? + var _6: String? + if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) } + var _7: [Api.Chat]? if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) } - var _7: [Api.User]? + var _8: [Api.User]? if let _ = reader.readInt32() { - _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = _6 != nil + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil let _c7 = _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.payments.SavedStarGifts.savedStarGifts(flags: _1!, count: _2!, chatNotificationsEnabled: _3, gifts: _4!, nextOffset: _5, chats: _6!, users: _7!) + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.payments.SavedStarGifts.savedStarGifts(flags: _1!, count: _2!, chatNotificationsEnabled: _3, pinnedToTop: _4, gifts: _5!, nextOffset: _6, chats: _7!, users: _8!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index a7f8b72dae..9c10a1bc0b 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -9717,6 +9717,26 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func toggleStarGiftsPinnedToTop(peer: Api.InputPeer, stargift: [Api.InputSavedStarGift]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(353626032) + peer.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(stargift.count)) + for item in stargift { + item.serialize(buffer, true) + } + return (FunctionDescription(name: "payments.toggleStarGiftsPinnedToTop", parameters: [("peer", String(describing: peer)), ("stargift", String(describing: stargift))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.payments { static func transferStarGift(stargift: Api.InputSavedStarGift, toId: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramCore/FlatBuffers/Package.swift b/submodules/TelegramCore/FlatBuffers/Package.swift new file mode 100644 index 0000000000..a5df82be59 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version:5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "FlatBuffers", + platforms: [.macOS(.v10_13)], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "FlatBuffers", + targets: ["FlatBuffers"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "FlatBuffers", + dependencies: [], + path: "Sources"), + ] +) diff --git a/submodules/TelegramCore/FlatSerialization/Package.swift b/submodules/TelegramCore/FlatSerialization/Package.swift new file mode 100644 index 0000000000..0f6939f96d --- /dev/null +++ b/submodules/TelegramCore/FlatSerialization/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version:5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "FlatSerialization", + platforms: [.macOS(.v10_13)], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "FlatSerialization", + targets: ["FlatSerialization"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + .package(name: "FlatBuffers", path: "../FlatBuffers") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "FlatSerialization", + dependencies: [ + .product(name: "FlatBuffers", package: "FlatBuffers", condition: nil), + ], + path: "Sources"), + ] +) diff --git a/submodules/TelegramCore/FlatSerialization/macOS/generate.sh b/submodules/TelegramCore/FlatSerialization/macOS/generate.sh index cb0b4987fb..ef391b5afc 100644 --- a/submodules/TelegramCore/FlatSerialization/macOS/generate.sh +++ b/submodules/TelegramCore/FlatSerialization/macOS/generate.sh @@ -3,10 +3,15 @@ # Default directories OUTPUT_DIR="" INPUT_DIR="" +BINARY_PATH="" # Parse command line arguments while [ "$#" -gt 0 ]; do case "$1" in + --binary) + BINARY_PATH="$2" + shift 2 + ;; --output) OUTPUT_DIR="$2" shift 2 @@ -28,6 +33,12 @@ if [ -z "$OUTPUT_DIR" ]; then exit 1 fi +# Validate output directory +if [ -z "$BINARY_PATH" ]; then + echo "Error: --binary argument is required" + exit 1 +fi + if [ ! -d "$OUTPUT_DIR" ]; then echo "Error: Output directory does not exist: $OUTPUT_DIR" exit 1 @@ -58,4 +69,4 @@ for model in $models; do flatc_input="$flatc_input $model" done -flatc --require-explicit-ids --swift -o "$OUTPUT_DIR" ${flatc_input} +$BINARY_PATH --require-explicit-ids --swift -o "$OUTPUT_DIR" ${flatc_input} diff --git a/submodules/TelegramCore/Package.swift b/submodules/TelegramCore/Package.swift index bd86af416d..9060a52b3b 100644 --- a/submodules/TelegramCore/Package.swift +++ b/submodules/TelegramCore/Package.swift @@ -15,6 +15,8 @@ let package = Package( dependencies: [ // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), + .package(name: "FlatBuffers", path: "./FlatBuffers"), + .package(name: "FlatSerialization", path: "./FlatSerialization"), .package(name: "Postbox", path: "../Postbox"), .package(name: "SSignalKit", path: "../SSignalKit"), .package(name: "MtProtoKit", path: "../MtProtoKit"), @@ -40,6 +42,8 @@ let package = Package( .product(name: "DarwinDirStat", package: "DarwinDirStat", condition: nil), .product(name: "Reachability", package: "Reachability", condition: nil), .product(name: "Emoji", package: "Emoji", condition: nil), + .product(name: "FlatBuffers", package: "FlatBuffers", condition: nil), + .product(name: "FlatSerialization", package: "FlatSerialization", condition: nil), .product(name: "EncryptionProvider", package: "EncryptionProvider", condition: nil)], path: "Sources"), ] diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index 12663f7841..ef367cdca9 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -190,6 +190,7 @@ private var declaredEncodables: Void = { declareEncodable(OutgoingScheduleInfoMessageAttribute.self, f: { OutgoingScheduleInfoMessageAttribute(decoder: $0) }) declareEncodable(UpdateMessageReactionsAction.self, f: { UpdateMessageReactionsAction(decoder: $0) }) declareEncodable(SendStarsReactionsAction.self, f: { SendStarsReactionsAction(decoder: $0) }) + declareEncodable(PostponeSendPaidMessageAction.self, f: { PostponeSendPaidMessageAction(decoder: $0) }) declareEncodable(RestrictedContentMessageAttribute.self, f: { RestrictedContentMessageAttribute(decoder: $0) }) declareEncodable(SendScheduledMessageImmediatelyAction.self, f: { SendScheduledMessageImmediatelyAction(decoder: $0) }) declareEncodable(EmbeddedMediaStickersMessageAttribute.self, f: { EmbeddedMediaStickersMessageAttribute(decoder: $0) }) diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index e27e35dfb3..03276a38be 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -904,7 +904,7 @@ extension StoreMessage { } if let paidMessageStars { - attributes.append(PaidStarsMessageAttribute(stars: StarsAmount(value: paidMessageStars, nanos: 0))) + attributes.append(PaidStarsMessageAttribute(stars: StarsAmount(value: paidMessageStars, nanos: 0), postponeSending: false)) } var entitiesAttribute: TextEntitiesMessageAttribute? diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift index b41bf14e64..4fac57c73d 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift @@ -129,6 +129,7 @@ public func standaloneSendEnqueueMessages( struct MessageResult { var result: PendingMessageUploadedContentResult var media: [Media] + var attributes: [MessageAttribute] } let signals: [Signal] = messages.map { message in @@ -178,7 +179,10 @@ public func standaloneSendEnqueueMessages( if message.isSilent { attributes.append(NotificationInfoMessageAttribute(flags: .muted)) } - + if let sendPaidMessageStars = message.sendPaidMessageStars { + attributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: false)) + } + let content = messageContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: { _, _, _, _ in return .single(nil) }, messageMediaPreuploadManager: MessageMediaPreuploadManager(), revalidationContext: MediaReferenceRevalidationContext(), forceReupload: false, isGrouped: false, passFetchProgress: true, forceNoBigParts: false, peerId: peerId, messageId: nil, attributes: attributes, text: text, media: media) @@ -191,7 +195,7 @@ public func standaloneSendEnqueueMessages( } return contentResult |> map { contentResult in - return MessageResult(result: contentResult, media: media) + return MessageResult(result: contentResult, media: media, attributes: attributes) } } @@ -201,7 +205,7 @@ public func standaloneSendEnqueueMessages( } |> mapToSignal { contentResults -> Signal in var progressSum: Float = 0.0 - var allResults: [(result: PendingMessageUploadedContentAndReuploadInfo, media: [Media])] = [] + var allResults: [(result: PendingMessageUploadedContentAndReuploadInfo, media: [Media], attributes: [MessageAttribute])] = [] var allDone = true for result in contentResults { switch result.result { @@ -209,13 +213,13 @@ public func standaloneSendEnqueueMessages( allDone = false progressSum += value.progress case let .content(content): - allResults.append((content, result.media)) + allResults.append((content, result.media, result.attributes)) } } if allDone { var sendSignals: [Signal] = [] - for (content, media) in allResults { + for (content, media, attributes) in allResults { var text: String = "" switch content.content { case let .text(textValue): @@ -235,7 +239,7 @@ public func standaloneSendEnqueueMessages( peerId: peerId, content: content, text: text, - attributes: [], + attributes: attributes, media: media, threadId: threadId )) @@ -328,6 +332,7 @@ private func sendUploadedMessageContent( var videoTimestamp: Int32? var sendAsPeerId: PeerId? var bubbleUpEmojiOrStickersets = false + var allowPaidStars: Int64? var flags: Int32 = 0 @@ -365,6 +370,8 @@ private func sendUploadedMessageContent( } else if let attribute = attribute as? ForwardVideoTimestampAttribute { flags |= Int32(1 << 20) videoTimestamp = attribute.timestamp + } else if let attribute = attribute as? PaidStarsMessageAttribute { + allowPaidStars = attribute.stars.value } } @@ -390,6 +397,11 @@ private func sendUploadedMessageContent( flags |= (1 << 13) } + if let _ = allowPaidStars { + flags |= 1 << 21 + } + + let dependencyTag: PendingMessageRequestDependencyTag? = nil//(messageId: messageId) let sendMessageRequest: Signal, MTRpcError> @@ -415,7 +427,7 @@ private func sendUploadedMessageContent( } } - sendMessageRequest = network.requestWithAdditionalInfo(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: nil), info: .acknowledgement, tag: dependencyTag) + sendMessageRequest = network.requestWithAdditionalInfo(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: allowPaidStars), info: .acknowledgement, tag: dependencyTag) case let .media(inputMedia, text): if bubbleUpEmojiOrStickersets { flags |= Int32(1 << 15) @@ -437,7 +449,7 @@ private func sendUploadedMessageContent( } } - sendMessageRequest = network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: nil), tag: dependencyTag) + sendMessageRequest = network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: allowPaidStars), tag: dependencyTag) |> map(NetworkRequestResult.result) case let .forward(sourceInfo): var topMsgId: Int32? @@ -447,7 +459,7 @@ private func sendUploadedMessageContent( } if let forwardSourceInfoAttribute = forwardSourceInfoAttribute, let sourcePeer = transaction.getPeer(forwardSourceInfoAttribute.messageId.peerId), let sourceInputPeer = apiInputPeer(sourcePeer) { - sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, videoTimestamp: videoTimestamp, allowPaidStars: nil), tag: dependencyTag) + sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars), tag: dependencyTag) |> map(NetworkRequestResult.result) } else { sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "internal")) @@ -473,7 +485,7 @@ private func sendUploadedMessageContent( } } - sendMessageRequest = network.request(Api.functions.messages.sendInlineBotResult(flags: flags, peer: inputPeer, replyTo: replyTo, randomId: uniqueId, queryId: chatContextResult.queryId, id: chatContextResult.id, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, allowPaidStars: nil)) + sendMessageRequest = network.request(Api.functions.messages.sendInlineBotResult(flags: flags, peer: inputPeer, replyTo: replyTo, randomId: uniqueId, queryId: chatContextResult.queryId, id: chatContextResult.id, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, allowPaidStars: allowPaidStars)) |> map(NetworkRequestResult.result) case .messageScreenshot: let replyTo: Api.InputReplyTo @@ -585,6 +597,7 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M var replyToStoryId: StoryId? var scheduleTime: Int32? var sendAsPeerId: PeerId? + var allowPaidStars: Int64? var flags: Int32 = 0 flags |= (1 << 7) @@ -609,6 +622,8 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M scheduleTime = attribute.scheduleTime } else if let attribute = attribute as? SendAsMessageAttribute { sendAsPeerId = attribute.peerId + } else if let attribute = attribute as? PaidStarsMessageAttribute { + allowPaidStars = attribute.stars.value } } @@ -622,6 +637,11 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M flags |= (1 << 13) } + if let _ = allowPaidStars { + flags |= 1 << 21 + } + + let sendMessageRequest: Signal switch content { case let .text(text): @@ -641,7 +661,7 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M replyTo = .inputReplyToMessage(flags: flags, replyToMsgId: threadId, topMsgId: threadId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil) } - sendMessageRequest = account.network.request(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: nil)) + sendMessageRequest = account.network.request(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: allowPaidStars)) |> `catch` { _ -> Signal in return .complete() } @@ -662,7 +682,7 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M replyTo = .inputReplyToMessage(flags: flags, replyToMsgId: threadId, topMsgId: threadId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil) } - sendMessageRequest = account.network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: nil)) + sendMessageRequest = account.network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: allowPaidStars)) |> `catch` { _ -> Signal in return .complete() } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index d6494b0923..a32d851fd5 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -331,6 +331,16 @@ public final class AccountStateManager { return self.forceSendPendingStarsReactionPipe.signal() } + fileprivate let forceSendPendingPaidMessagePipe = ValuePipe() + public var forceSendPendingPaidMessage: Signal { + return self.forceSendPendingPaidMessagePipe.signal() + } + + fileprivate let commitSendPendingPaidMessagePipe = ValuePipe() + public var commitSendPendingPaidMessage: Signal { + return self.commitSendPendingPaidMessagePipe.signal() + } + fileprivate let sentScheduledMessageIdsPipe = ValuePipe>() public var sentScheduledMessageIds: Signal, NoError> { return self.sentScheduledMessageIdsPipe.signal() @@ -1951,6 +1961,18 @@ public final class AccountStateManager { } } + var forceSendPendingPaidMessage: Signal { + return self.impl.signalWith { impl, subscriber in + return impl.forceSendPendingPaidMessage.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) + } + } + + var commitSendPendingPaidMessage: Signal { + return self.impl.signalWith { impl, subscriber in + return impl.commitSendPendingPaidMessage.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) + } + } + public var sentScheduledMessageIds: Signal, NoError> { return self.impl.signalWith { impl, subscriber in return impl.sentScheduledMessageIds.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) @@ -1963,6 +1985,19 @@ public final class AccountStateManager { } } + + func forceSendPendingPaidMessage(peerId: PeerId) { + self.impl.with { impl in + impl.forceSendPendingPaidMessagePipe.putNext(peerId) + } + } + + func commitSendPendingPaidMessage(messageId: MessageId) { + self.impl.with { impl in + impl.commitSendPendingPaidMessagePipe.putNext(messageId) + } + } + var updateConfigRequested: (() -> Void)? var isPremiumUpdated: (() -> Void)? diff --git a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift index ba23451233..48ff0f9e03 100644 --- a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift @@ -88,6 +88,9 @@ final class AccountTaskManager { tasks.add(managedSynchronizeMarkAllUnseenReactionsOperations(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start()) tasks.add(managedApplyPendingMessageReactionsActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start()) tasks.add(managedApplyPendingMessageStarsReactionsActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start()) + + tasks.add(managedApplyPendingPaidMessageActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start()) + tasks.add(managedSynchronizeEmojiKeywordsOperations(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedApplyPendingScheduledMessagesActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start()) tasks.add(managedSynchronizeAvailableReactions(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) diff --git a/submodules/TelegramCore/Sources/State/PaidMessages.swift b/submodules/TelegramCore/Sources/State/PaidMessages.swift index 95314d227c..eb12453c9a 100644 --- a/submodules/TelegramCore/Sources/State/PaidMessages.swift +++ b/submodules/TelegramCore/Sources/State/PaidMessages.swift @@ -89,4 +89,169 @@ func _internal_updateChannelPaidMessagesStars(account: Account, peerId: PeerId, |> switchToLatest } +public final class PostponeSendPaidMessageAction: PendingMessageActionData { + public let randomId: Int64 + + public init(randomId: Int64) { + self.randomId = randomId + } + + public init(decoder: PostboxDecoder) { + self.randomId = decoder.decodeInt64ForKey("id", orElse: 0) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt64(self.randomId, forKey: "id") + } + + public func isEqual(to: PendingMessageActionData) -> Bool { + if let other = to as? PostponeSendPaidMessageAction { + if self.randomId != other.randomId { + return false + } + return true + } else { + return false + } + } +} +private final class ManagedApplyPendingPaidMessageActionsHelper { + var operationDisposables: [MessageId: (PendingMessageActionData, Disposable)] = [:] + + func update(entries: [PendingMessageActionsEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) { + var disposeOperations: [Disposable] = [] + var beginOperations: [(PendingMessageActionsEntry, MetaDisposable)] = [] + + var hasRunningOperationForPeerId = Set() + var validIds = Set() + for entry in entries { + if let current = self.operationDisposables[entry.id], !current.0.isEqual(to: entry.action) { + self.operationDisposables.removeValue(forKey: entry.id) + disposeOperations.append(current.1) + } + + if !hasRunningOperationForPeerId.contains(entry.id.peerId) { + hasRunningOperationForPeerId.insert(entry.id.peerId) + validIds.insert(entry.id) + + let disposable = MetaDisposable() + beginOperations.append((entry, disposable)) + self.operationDisposables[entry.id] = (entry.action, disposable) + } + } + + var removeMergedIds: [MessageId] = [] + for (id, actionAndDisposable) in self.operationDisposables { + if !validIds.contains(id) { + removeMergedIds.append(id) + disposeOperations.append(actionAndDisposable.1) + } + } + + for id in removeMergedIds { + self.operationDisposables.removeValue(forKey: id) + } + + return (disposeOperations, beginOperations) + } + + func reset() -> [Disposable] { + let disposables = Array(self.operationDisposables.values.map(\.1)) + self.operationDisposables.removeAll() + return disposables + } +} + +private func withTakenStarsAction(postbox: Postbox, type: PendingMessageActionType, id: MessageId, _ f: @escaping (Transaction, PendingMessageActionsEntry?) -> Signal) -> Signal { + return postbox.transaction { transaction -> Signal in + var result: PendingMessageActionsEntry? + + if let action = transaction.getPendingMessageAction(type: type, id: id) as? PostponeSendPaidMessageAction { + result = PendingMessageActionsEntry(id: id, action: action) + } + + return f(transaction, result) + } + |> switchToLatest +} + +private func sendPostponedPaidMessage(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, id: MessageId) -> Signal { + stateManager.commitSendPendingPaidMessage(messageId: id) + return postbox.transaction { transaction -> Void in + transaction.setPendingMessageAction(type: .sendPostponedPaidMessage, id: id, action: nil) + } + |> ignoreValues +} + +func managedApplyPendingPaidMessageActions(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal { + return Signal { _ in + let helper = Atomic(value: ManagedApplyPendingPaidMessageActionsHelper()) + + let actionsKey = PostboxViewKey.pendingMessageActions(type: .sendPostponedPaidMessage) + let disposable = postbox.combinedView(keys: [actionsKey]).start(next: { view in + var entries: [PendingMessageActionsEntry] = [] + if let v = view.views[actionsKey] as? PendingMessageActionsView { + entries = v.entries + } + + let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) in + return helper.update(entries: entries) + } + + for disposable in disposeOperations { + disposable.dispose() + } + + for (entry, disposable) in beginOperations { + let signal = withTakenStarsAction(postbox: postbox, type: .sendPostponedPaidMessage, id: entry.id, { transaction, entry -> Signal in + if let entry = entry { + if let _ = entry.action as? PostponeSendPaidMessageAction { + let triggerSignal: Signal = stateManager.forceSendPendingPaidMessage + |> filter { + $0 == entry.id.peerId + } + |> map { _ -> Void in + return Void() + } + |> take(1) + |> timeout(5.0, queue: .mainQueue(), alternate: .single(Void())) + + return triggerSignal + |> mapToSignal { _ -> Signal in + return sendPostponedPaidMessage(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, id: entry.id) + } + } else { + assertionFailure() + } + } + return .complete() + }) + |> then( + postbox.transaction { transaction -> Void in + transaction.setPendingMessageAction(type: .sendPostponedPaidMessage, id: entry.id, action: nil) + } + |> ignoreValues + ) + + disposable.set(signal.start()) + } + }) + + return ActionDisposable { + let disposables = helper.with { helper -> [Disposable] in + return helper.reset() + } + for disposable in disposables { + disposable.dispose() + } + disposable.dispose() + } + } +} + +func _internal_forceSendPostponedPaidMessage(account: Account, peerId: PeerId) -> Signal { + account.stateManager.forceSendPendingPaidMessage(peerId: peerId) + + return .complete() +} diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift index ab08a63bc1..732f492b57 100644 --- a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift @@ -63,6 +63,8 @@ private final class PendingMessageContext { var error: PendingMessageFailureReason? var statusSubscribers = Bag<(PendingMessageStatus?, PendingMessageFailureReason?) -> Void>() var forcedReuploadOnce: Bool = false + let postponeDisposable = MetaDisposable() + var postponeSending = false } public enum PendingMessageFailureReason { @@ -486,7 +488,10 @@ public final class PendingMessageManager { for (messageContext, message, type, contentUploadSignal) in messagesToUpload { - if strongSelf.canBeginUploadingMessage(id: message.id, type: type) { + if let paidStarsAttribute = message.paidStarsAttribute, paidStarsAttribute.postponeSending { + strongSelf.beginWaitingForPostponedMessageCommit(messageContext: messageContext, id: message.id) + } + if strongSelf.canBeginUploadingMessage(id: message.id, type: type), !messageContext.postponeSending { strongSelf.beginUploadingMessage(messageContext: messageContext, id: message.id, threadId: message.threadId, groupId: message.groupingKey, uploadSignal: contentUploadSignal) } else { messageContext.state = .waitingForUploadToStart(groupId: message.groupingKey, upload: contentUploadSignal) @@ -663,6 +668,33 @@ public final class PendingMessageManager { messageContext.state = .collectingInfo(message: message) } + private func beginWaitingForPostponedMessageCommit(messageContext: PendingMessageContext, id: MessageId) { + messageContext.postponeSending = true + + let signal: Signal = self.postbox.transaction { transaction -> Void in + transaction.setPendingMessageAction(type: .sendPostponedPaidMessage, id: id, action: PostponeSendPaidMessageAction(randomId: Int64.random(in: Int64.min ... Int64.max))) + } + |> mapToSignal { _ in + return self.stateManager.commitSendPendingPaidMessage + |> filter { + $0 == id + } + |> take(1) + |> map { _ in + Void() + } + } + |> deliverOn(self.queue) + + messageContext.postponeDisposable.set(signal.start(next: { [weak self] _ in + guard let self else { + return + } + messageContext.postponeSending = false + self.updateWaitingUploads(peerId: id.peerId) + })) + } + private func beginUploadingMessage(messageContext: PendingMessageContext, id: MessageId, threadId: Int64?, groupId: Int64?, uploadSignal: Signal) { messageContext.state = .uploading(groupId: groupId) @@ -740,7 +772,7 @@ public final class PendingMessageManager { loop: for contextId in messageIdsForPeer { let context = self.messageContexts[contextId]! if case let .waitingForUploadToStart(groupId, uploadSignal) = context.state { - if self.canBeginUploadingMessage(id: contextId, type: context.contentType ?? .media) { + if self.canBeginUploadingMessage(id: contextId, type: context.contentType ?? .media), !context.postponeSending { context.state = .uploading(groupId: groupId) let status = PendingMessageStatus(isRunning: true, progress: PendingMessageStatus.Progress(progress: 0.0)) context.status = status diff --git a/submodules/TelegramCore/Sources/SyncCore/PaidStarsMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/PaidStarsMessageAttribute.swift index ecf7d70aac..f784d5031a 100644 --- a/submodules/TelegramCore/Sources/SyncCore/PaidStarsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/PaidStarsMessageAttribute.swift @@ -4,20 +4,24 @@ import TelegramApi public final class PaidStarsMessageAttribute: Equatable, MessageAttribute { public let stars: StarsAmount + public let postponeSending: Bool - public init(stars: StarsAmount) { + public init(stars: StarsAmount, postponeSending: Bool) { self.stars = stars + self.postponeSending = postponeSending } required public init(decoder: PostboxDecoder) { self.stars = decoder.decodeCodable(StarsAmount.self, forKey: "s") ?? StarsAmount(value: 0, nanos: 0) + self.postponeSending = decoder.decodeBoolForKey("ps", orElse: false) } public func encode(_ encoder: PostboxEncoder) { encoder.encodeCodable(self.stars, forKey: "s") + encoder.encodeBool(self.postponeSending, forKey: "ps") } public static func ==(lhs: PaidStarsMessageAttribute, rhs: PaidStarsMessageAttribute) -> Bool { - return lhs.stars == rhs.stars + return lhs.stars == rhs.stars && lhs.postponeSending == rhs.postponeSending } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift index 45408d889e..89e671f7f9 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift @@ -25,6 +25,7 @@ public struct CachedChannelFlags: OptionSet { public static let paidMediaAllowed = CachedChannelFlags(rawValue: 1 << 11) public static let canViewStarsRevenue = CachedChannelFlags(rawValue: 1 << 12) public static let starGiftsAvailable = CachedChannelFlags(rawValue: 1 << 13) + public static let paidMessagesAvailable = CachedChannelFlags(rawValue: 1 << 14) } public struct CachedChannelParticipantsSummary: PostboxCoding, Equatable { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index f19036ecf6..d4d2c44847 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -191,6 +191,7 @@ public extension PendingMessageActionType { static let sendScheduledMessageImmediately = PendingMessageActionType(rawValue: 2) static let readReaction = PendingMessageActionType(rawValue: 3) static let sendStarsReaction = PendingMessageActionType(rawValue: 4) + static let sendPostponedPaidMessage = PendingMessageActionType(rawValue: 5) } public let peerIdNamespacesWithInitialCloudMessageHoles = [Namespaces.Peer.CloudUser, Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel] diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index 71ed10b3ac..21e053560e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -375,22 +375,18 @@ public extension TelegramEngine.EngineData.Item { } var key: PostboxViewKey { - return .cachedPeerData(peerId: self.id) + return .peer(peerId: self.id, components: [.cachedData]) } func extract(view: PostboxView) -> Result { - guard let view = view as? CachedPeerDataView else { + guard let view = view as? PeerView else { preconditionFailure() } - guard let cachedPeerData = view.cachedPeerData else { - return nil - } - switch cachedPeerData { - case let user as CachedUserData: - return user.sendPaidMessageStars - case let channel as CachedChannelData: + if let cachedPeerData = view.cachedData as? CachedUserData { + return cachedPeerData.sendPaidMessageStars + } else if let channel = peerViewMainPeer(view) as? TelegramChannel { return channel.sendPaidMessageStars - default: + } else { return nil } } @@ -861,6 +857,38 @@ public extension TelegramEngine.EngineData.Item { } } + public struct PeerSettings: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Optional + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedUserData { + return cachedData.peerStatusSettings + } else if let cachedData = view.cachedPeerData as? CachedChannelData { + return cachedData.peerStatusSettings + } else if let cachedData = view.cachedPeerData as? CachedGroupData { + return cachedData.peerStatusSettings + } else { + return nil + } + } + } + public struct AreVideoCallsAvailable: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { public typealias Result = Bool diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift index c93f33e990..3302c7fafd 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift @@ -2,15 +2,15 @@ import Foundation import Postbox import SwiftSignalKit -func _internal_enqueueOutgoingMessageWithChatContextResult(account: Account, to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> Bool { - guard let message = _internal_outgoingMessageWithChatContextResult(to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId) else { +func _internal_enqueueOutgoingMessageWithChatContextResult(account: Account, to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, sendPaidMessageStars: StarsAmount?, postpone: Bool, correlationId: Int64?) -> Bool { + guard let message = _internal_outgoingMessageWithChatContextResult(to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, sendPaidMessageStars: sendPaidMessageStars, postpone: postpone, correlationId: correlationId) else { return false } let _ = enqueueMessages(account: account, peerId: peerId, messages: [message]).start() return true } -func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> EnqueueMessage? { +func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, sendPaidMessageStars: StarsAmount?, postpone: Bool, correlationId: Int64?) -> EnqueueMessage? { var replyToMessageId = replyToMessageId if replyToMessageId == nil, let threadId = threadId { replyToMessageId = EngineMessageReplySubject(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: MessageId.Id(clamping: threadId)), quote: nil) @@ -32,6 +32,9 @@ func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: if silentPosting { attributes.append(NotificationInfoMessageAttribute(flags: .muted)) } + if let sendPaidMessageStars { + attributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: postpone)) + } switch result.message { case let .auto(caption, entities, replyMarkup): if let entities = entities { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index d06770b90d..2ae4018184 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -247,13 +247,14 @@ public extension TelegramEngine { storyId: StoryId? = nil, content: EngineOutgoingMessageContent, silentPosting: Bool = false, - scheduleTime: Int32? = nil + scheduleTime: Int32? = nil, + sendPaidMessageStars: StarsAmount? = nil ) -> Signal<[MessageId?], NoError> { var message: EnqueueMessage? if case let .preparedInlineMessage(preparedInlineMessage) = content { - message = self.outgoingMessageWithChatContextResult(to: peerId, threadId: nil, botId: preparedInlineMessage.botId, result: preparedInlineMessage.result, replyToMessageId: replyToMessageId, replyToStoryId: storyId, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: nil) + message = self.outgoingMessageWithChatContextResult(to: peerId, threadId: nil, botId: preparedInlineMessage.botId, result: preparedInlineMessage.result, replyToMessageId: replyToMessageId, replyToStoryId: storyId, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime, sendPaidMessageStars: sendPaidMessageStars, postpone: false, correlationId: nil) } else if case let .contextResult(results, result) = content { - message = self.outgoingMessageWithChatContextResult(to: peerId, threadId: nil, botId: results.botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: storyId, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: nil) + message = self.outgoingMessageWithChatContextResult(to: peerId, threadId: nil, botId: results.botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: storyId, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime, sendPaidMessageStars: sendPaidMessageStars, postpone: false, correlationId: nil) } else { var attributes: [MessageAttribute] = [] if silentPosting { @@ -262,6 +263,9 @@ public extension TelegramEngine { if let scheduleTime = scheduleTime { attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime)) } + if let sendPaidMessageStars { + attributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: false)) + } var text: String = "" var mediaReference: AnyMediaReference? @@ -301,12 +305,12 @@ public extension TelegramEngine { ) } - public func enqueueOutgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject? = nil, replyToStoryId: StoryId? = nil, hideVia: Bool = false, silentPosting: Bool = false, scheduleTime: Int32? = nil, correlationId: Int64? = nil) -> Bool { - return _internal_enqueueOutgoingMessageWithChatContextResult(account: self.account, to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId) + public func enqueueOutgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject? = nil, replyToStoryId: StoryId? = nil, hideVia: Bool = false, silentPosting: Bool = false, scheduleTime: Int32? = nil, sendPaidMessageStars: StarsAmount?, postpone: Bool = false, correlationId: Int64? = nil) -> Bool { + return _internal_enqueueOutgoingMessageWithChatContextResult(account: self.account, to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, sendPaidMessageStars: sendPaidMessageStars, postpone: postpone, correlationId: correlationId) } - public func outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> EnqueueMessage? { - return _internal_outgoingMessageWithChatContextResult(to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId) + public func outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, sendPaidMessageStars: StarsAmount?, postpone: Bool, correlationId: Int64?) -> EnqueueMessage? { + return _internal_outgoingMessageWithChatContextResult(to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, sendPaidMessageStars: sendPaidMessageStars, postpone: postpone, correlationId: correlationId) } public func setMessageReactions( @@ -349,6 +353,10 @@ public extension TelegramEngine { let _ = _internal_forceSendPendingSendStarsReaction(account: self.account, messageId: id).startStandalone() } + public func forceSendPostponedPaidMessage(peerId: EnginePeer.Id) { + let _ = _internal_forceSendPostponedPaidMessage(account: self.account, peerId: peerId).startStandalone() + } + public func updateStarsReactionPrivacy(id: EngineMessage.Id, privacy: TelegramPaidReactionPrivacy) -> Signal { return _internal_updateStarsReactionPrivacy(account: self.account, messageId: id, privacy: privacy) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 4f4c241a45..653c79d8f4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -1064,7 +1064,8 @@ private final class ProfileGiftsContextImpl { } return postbox.transaction { transaction -> ([ProfileGiftsContext.State.StarGift], Int32, String?, Bool?) in switch result { - case let .savedStarGifts(_, count, apiNotificationsEnabled, apiGifts, nextOffset, chats, users): + case let .savedStarGifts(_, count, apiNotificationsEnabled, pinnedToTop, apiGifts, nextOffset, chats, users): + let _ = pinnedToTop let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift index 27ad703429..d303743a59 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift @@ -99,8 +99,8 @@ func _internal_addGroupMember(account: Account, peerId: PeerId, memberId: PeerId } }) } - - return TelegramInvitePeersResult(forbiddenPeers: missingInviteesValue.compactMap { invitee -> TelegramForbiddenInvitePeer? in + + let result = TelegramInvitePeersResult(forbiddenPeers: missingInviteesValue.compactMap { invitee -> TelegramForbiddenInvitePeer? in switch invitee { case let .missingInvitee(flags, userId): guard let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))) else { @@ -113,6 +113,10 @@ func _internal_addGroupMember(account: Account, peerId: PeerId, memberId: PeerId ) } }) + + let _ = _internal_updateIsPremiumRequiredToContact(account: account, peerIds: result.forbiddenPeers.map { $0.peer.id }).startStandalone() + + return result } |> mapError { _ -> AddGroupMemberError in } |> mapToSignal { result -> Signal in @@ -186,6 +190,8 @@ func _internal_addChannelMember(account: Account, peerId: PeerId, memberId: Peer switch result { case let .invitedUsers(updates, missingInvitees): if case let .missingInvitee(flags, _) = missingInvitees.first { + let _ = _internal_updateIsPremiumRequiredToContact(account: account, peerIds: [memberPeer.id]).startStandalone() + return .fail(.restricted(TelegramForbiddenInvitePeer( peer: EnginePeer(memberPeer), canInviteWithPremium: (flags & (1 << 0)) != 0, @@ -302,7 +308,7 @@ func _internal_addChannelMembers(account: Account, peerId: PeerId, memberIds: [P account.viewTracker.forceUpdateCachedPeerData(peerId: peerId) return account.postbox.transaction { transaction -> TelegramInvitePeersResult in - return TelegramInvitePeersResult(forbiddenPeers: missingInviteesValue.compactMap { invitee -> TelegramForbiddenInvitePeer? in + let result = TelegramInvitePeersResult(forbiddenPeers: missingInviteesValue.compactMap { invitee -> TelegramForbiddenInvitePeer? in switch invitee { case let .missingInvitee(flags, userId): guard let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))) else { @@ -315,6 +321,8 @@ func _internal_addChannelMembers(account: Account, peerId: PeerId, memberIds: [P ) } }) + let _ = _internal_updateIsPremiumRequiredToContact(account: account, peerIds: result.forbiddenPeers.map { $0.peer.id }).startStandalone() + return result } |> castError(AddChannelMemberError.self) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index c21d258bb2..9c68e06e69 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -620,6 +620,10 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee if (flags2 & Int32(1 << 19)) != 0 { channelFlags.insert(.starGiftsAvailable) } + if (flags2 & Int32(1 << 20)) != 0 { + channelFlags.insert(.paidMessagesAvailable) + } + let sendAsPeerId = defaultSendAs?.peerId let linkedDiscussionPeerId: PeerId? diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 519b939af8..8f5c9c16c7 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -238,6 +238,7 @@ private struct ApplicationSpecificNoticeKeys { private static let groupEmojiPackNamespace: Int32 = 9 private static let dismissedBirthdayPremiumGiftTipNamespace: Int32 = 10 private static let displayedPeerVerificationNamespace: Int32 = 11 + private static let dismissedPaidMessageWarningNamespace: Int32 = 11 static func inlineBotLocationRequestNotice(peerId: PeerId) -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: inlineBotLocationRequestNamespace), key: noticeKey(peerId: peerId, key: 0)) @@ -523,6 +524,10 @@ private struct ApplicationSpecificNoticeKeys { return NoticeEntryKey(namespace: noticeNamespace(namespace: displayedPeerVerificationNamespace), key: noticeKey(peerId: peerId, key: 0)) } + static func dismissedPaidMessageWarning(peerId: PeerId) -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: dismissedPaidMessageWarningNamespace), key: noticeKey(peerId: peerId, key: 0)) + } + static func monetizationIntroDismissed() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.monetizationIntroDismissed.key) } @@ -2176,6 +2181,28 @@ public struct ApplicationSpecificNotice { |> ignoreValues } + public static func dismissedPaidMessageWarningNamespace(accountManager: AccountManager, peerId: PeerId) -> Signal { + return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedPaidMessageWarning(peerId: peerId)) + |> map { view -> Int64? in + if let counter = view.value?.get(ApplicationSpecificCounterNotice.self) { + return Int64(counter.value) + } else { + return nil + } + } + } + + public static func setDismissedPaidMessageWarningNamespace(accountManager: AccountManager, peerId: PeerId, amount: Int64?) -> Signal { + return accountManager.transaction { transaction -> Void in + if let amount, let entry = CodableEntry(ApplicationSpecificCounterNotice(value: Int32(amount))) { + transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedPaidMessageWarning(peerId: peerId), entry) + } else { + transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedPaidMessageWarning(peerId: peerId), nil) + } + } + |> ignoreValues + } + public static func setMonetizationIntroDismissed(accountManager: AccountManager) -> Signal { return accountManager.transaction { transaction -> Void in if let entry = CodableEntry(ApplicationSpecificBoolNotice()) { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 18436a6210..eaf7616a85 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -313,12 +313,16 @@ public enum PresentationResourceKey: Int32 { case chatBubbleCloseIcon case chatEmptyStateStarIcon + case chatPlaceholderStarIcon case avatarPremiumLockBadgeBackground case avatarPremiumLockBadge case shareAvatarPremiumLockBadgeBackground case shareAvatarPremiumLockBadge + case shareAvatarStarsLockBadgeBackground + case shareAvatarStarsLockBadgeInnerBackground + case sharedLinkIcon case hideIconImage diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index be46ec6bc5..3c9f6288d0 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -1351,4 +1351,19 @@ public struct PresentationResourcesChat { return nil }) } + + public static func chatPlaceholderStarIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatPlaceholderStarIcon.rawValue, { theme in + if let image = UIImage(bundleImageName: "Premium/Stars/ButtonStar") { + return generateImage(image.size, contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + if let cgImage = image.cgImage { + context.draw(cgImage, in: bounds.offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel), byTiling: false) + } + }) + } + return nil + }) + } } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift index 9522c0706a..4700e4fb7d 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift @@ -488,6 +488,29 @@ public struct PresentationResourcesChatList { }) } + public static func shareAvatarStarsLockBadgeBackground(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.shareAvatarStarsLockBadgeBackground.rawValue, { theme in + return generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + context.setFillColor(UIColor.white.cgColor) + let rect = CGRect(origin: .zero, size: CGSize(width: 20.0, height: 18.0 + UIScreenPixel)).insetBy(dx: 1.0 - UIScreenPixel, dy: 0.0) + context.addPath(UIBezierPath(roundedRect: rect, cornerRadius: rect.height / 2.0).cgPath) + context.fillPath() + })?.withRenderingMode(.alwaysTemplate).stretchableImage(withLeftCapWidth: 10, topCapHeight: 10) + }) + } + + public static func shareAvatarStarsLockBadgeInnerBackground(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.shareAvatarStarsLockBadgeInnerBackground.rawValue, { theme in + return generateImage(CGSize(width: 20.0, height: 16.0), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + context.setFillColor(UIColor.white.cgColor) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(width: 20.0, height: 15.0)), cornerRadius: 7.5).cgPath) + context.fillPath() + })?.withRenderingMode(.alwaysTemplate).stretchableImage(withLeftCapWidth: 10, topCapHeight: 0) + }) + } + public static func shareAvatarPremiumLockBadge(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.shareAvatarPremiumLockBadge.rawValue, { theme in return generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context in diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 0c6e86b13c..fdf187168a 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -79,7 +79,7 @@ private func peerDisplayTitles(_ peers: [Peer], strings: PresentationStrings, na } } -public func universalServiceMessageString(presentationData: (PresentationTheme, TelegramWallpaper)?, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id, forChatList: Bool, forForumOverview: Bool, forAdditionalServiceMessage: Bool = false) -> NSAttributedString? { +public func universalServiceMessageString(presentationData: (PresentationTheme, TelegramWallpaper)?, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, messageCount: Int? = nil, accountPeerId: EnginePeer.Id, forChatList: Bool, forForumOverview: Bool, forAdditionalServiceMessage: Bool = false) -> NSAttributedString? { var attributedString: NSAttributedString? let primaryTextColor: UIColor @@ -92,6 +92,33 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, let bodyAttributes = MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [:]) let boldAttributes = MarkdownAttributeSet(font: titleBoldFont, textColor: primaryTextColor, additionalAttributes: [:]) + if !forAdditionalServiceMessage { + for attribute in message.attributes { + if let attribute = attribute as? PaidStarsMessageAttribute { + let messageCount = Int32(messageCount ?? 1) + let price = strings.Notification_PaidMessage_Stars(Int32(attribute.stars.value) * messageCount) + if message.author?.id == accountPeerId { + if messageCount > 1 { + let messagesString = strings.Notification_PaidMessage_Messages(messageCount) + return addAttributesToStringWithRanges(strings.Notification_PaidMessageYouMany(price, messagesString)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: boldAttributes]) + } else { + return addAttributesToStringWithRanges(strings.Notification_PaidMessageYou(price)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + } + } else { + let compactAuthorName = message.author?.compactDisplayTitle ?? "" + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)]) + attributes[1] = boldAttributes + if messageCount > 1 { + let messagesString = strings.Notification_PaidMessage_Messages(messageCount) + return addAttributesToStringWithRanges(strings.Notification_PaidMessageMany(compactAuthorName, price, messagesString)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } else { + return addAttributesToStringWithRanges(strings.Notification_PaidMessage(compactAuthorName, price)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } + } + } + } + } + for media in message.media { if let action = media as? TelegramMediaAction { let authorName = message.author?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) ?? "" @@ -1150,11 +1177,11 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, let attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_TransferToChannelYou(peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes) } else { - let senderPeerName = EnginePeer(targetPeer).compactDisplayTitle + let targetPeerName = EnginePeer(targetPeer).compactDisplayTitle peerName = EnginePeer(peer).compactDisplayTitle - peerIds = [(0, senderId), (1, peerId)] + peerIds = [(0, peer.id), (1, targetPeer.id)] let attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) - attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_TransferToChannel(senderPeerName, peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes) + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_TransferToChannel(peerName, targetPeerName)._tuple, body: bodyAttributes, argumentAttributes: attributes) } } else { peerName = EnginePeer(peer).compactDisplayTitle @@ -1169,18 +1196,6 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } } } - //TODO:release - /*case let .paidMessage(stars): - if message.author?.id == accountPeerId { - let starsString = strings.Notification_PaidMessage_Stars(Int32(stars)) - let resultTitleString = strings.Notification_PaidMessageYou(starsString) - attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) - } else { - let peerName = message.author?.compactDisplayTitle ?? "" - let starsString = strings.Notification_PaidMessage_Stars(Int32(stars)) - let resultTitleString = strings.Notification_PaidMessage(peerName, starsString) - attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: boldAttributes]) - }*/ case .unknown: attributedString = nil } diff --git a/submodules/TelegramStringFormatting/Sources/TonFormat.swift b/submodules/TelegramStringFormatting/Sources/TonFormat.swift index ede7945cad..de64f93e82 100644 --- a/submodules/TelegramStringFormatting/Sources/TonFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/TonFormat.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import TelegramCore import TelegramPresentationData let walletAddressLength: Int = 48 @@ -78,6 +79,20 @@ public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDate return balanceText } +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 + if fraction > 0.0 { + balanceText.append(dateTimeFormat.decimalSeparator) + balanceText.append("\(Int32(fraction))") + } + if amount.value < 0 { + } else if showPlus { + balanceText.insert("+", at: balanceText.startIndex) + } + return balanceText +} + private let invalidAddressCharacters = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_=").inverted public func isValidTonAddress(_ address: String, exactLength: Bool = false) -> Bool { if address.count > walletAddressLength || address.rangeOfCharacter(from: invalidAddressCharacters) != nil { @@ -89,10 +104,9 @@ public func isValidTonAddress(_ address: String, exactLength: Bool = false) -> B return true } -private let amountDelimeterCharacters = CharacterSet(charactersIn: "0123456789-+").inverted -public func tonAmountAttributedString(_ string: String, integralFont: UIFont, fractionalFont: UIFont, color: UIColor) -> NSAttributedString { +public func tonAmountAttributedString(_ string: String, integralFont: UIFont, fractionalFont: UIFont, color: UIColor, decimalSeparator: String) -> NSAttributedString { let result = NSMutableAttributedString() - if let range = string.rangeOfCharacter(from: amountDelimeterCharacters) { + if let range = string.range(of: decimalSeparator) { let integralPart = String(string[..] = [] items.append( AnyComponentWithIdentity( @@ -186,7 +205,7 @@ private final class ScrollContent: CombinedComponent { component: AnyComponent(ParagraphComponent( title: strings.AdsInfo_Respect_Title, titleColor: textColor, - text: component.mode == .bot ? strings.AdsInfo_Bot_Respect_Text : strings.AdsInfo_Respect_Text, + text: respectText, textColor: secondaryTextColor, accentColor: linkColor, iconName: "Ads/Privacy", @@ -194,27 +213,31 @@ private final class ScrollContent: CombinedComponent { )) ) ) - items.append( - AnyComponentWithIdentity( - id: "split", - component: AnyComponent(ParagraphComponent( - title: component.mode == .bot ? strings.AdsInfo_Bot_Split_Title : strings.AdsInfo_Split_Title, - titleColor: textColor, - text: component.mode == .bot ? strings.AdsInfo_Bot_Split_Text : strings.AdsInfo_Split_Text, - textColor: secondaryTextColor, - accentColor: linkColor, - iconName: "Ads/Split", - iconColor: linkColor - )) + if case .search = component.mode { + + } else { + items.append( + AnyComponentWithIdentity( + id: "split", + component: AnyComponent(ParagraphComponent( + title: component.mode == .bot ? strings.AdsInfo_Bot_Split_Title : strings.AdsInfo_Split_Title, + titleColor: textColor, + text: component.mode == .bot ? strings.AdsInfo_Bot_Split_Text : strings.AdsInfo_Split_Text, + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Ads/Split", + iconColor: linkColor + )) + ) ) - ) + } items.append( AnyComponentWithIdentity( id: "ads", component: AnyComponent(ParagraphComponent( title: strings.AdsInfo_Ads_Title, titleColor: textColor, - text: component.mode == .bot ? strings.AdsInfo_Bot_Ads_Text : strings.AdsInfo_Ads_Text("\(premiumConfiguration.minChannelRestrictAdsLevel)").string, + text: adsText, textColor: secondaryTextColor, accentColor: linkColor, iconName: "Premium/BoostPerk/NoAds", @@ -253,7 +276,7 @@ private final class ScrollContent: CombinedComponent { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) } - var infoString = component.mode == .bot ? strings.AdsInfo_Bot_Launch_Text : strings.AdsInfo_Launch_Text + var infoString = infoRawText if let spaceRegex { let nsRange = NSRange(infoString.startIndex..., in: infoString) let matches = spaceRegex.matches(in: infoString, options: [], range: nsRange) diff --git a/submodules/TelegramUI/Components/AvatarBackground/Sources/AvatarBackground.swift b/submodules/TelegramUI/Components/AvatarBackground/Sources/AvatarBackground.swift index e7b6743ce9..a6d9601c10 100644 --- a/submodules/TelegramUI/Components/AvatarBackground/Sources/AvatarBackground.swift +++ b/submodules/TelegramUI/Components/AvatarBackground/Sources/AvatarBackground.swift @@ -5,27 +5,65 @@ import GradientBackground public enum AvatarBackground: Equatable { public static let defaultBackgrounds: [AvatarBackground] = [ - .gradient([0xFF5A7FFF, 0xFF2CA0F2, 0xFF4DFF89, 0xFF6BFCEB]), - .gradient([0xFFFF011D, 0xFFFF530D, 0xFFFE64DC, 0xFFFFDC61]), - .gradient([0xFFFE64DC, 0xFFFF6847, 0xFFFFDD02, 0xFFFFAE10]), - .gradient([0xFF84EC00, 0xFF00B7C2, 0xFF00C217, 0xFFFFE600]), - .gradient([0xFF86B0FF, 0xFF35FFCF, 0xFF69FFFF, 0xFF76DEFF]), - .gradient([0xFFFAE100, 0xFFFF54EE, 0xFFFC2B78, 0xFFFF52D9]), - .gradient([0xFF73A4FF, 0xFF5F55FF, 0xFFFF49F8, 0xFFEC76FF]), + .gradient([0xFF5bd1ca, 0xFF538edb], false), + .gradient([0xFF61dba8, 0xFF52abd6], false), + .gradient([0xFFbdcb57, 0xFF4abe6e], false), + .gradient([0xFFd971bf, 0xFF986ce9], false), + .gradient([0xFFee8c56, 0xFFec628f], false), + .gradient([0xFFf2994f, 0xFFe76667], false), + .gradient([0xFFf0b948, 0xFFef7e4b], false), + + .gradient([0xFF94A3B0, 0xFF6C7B87], true), + .gradient([0xFF949487, 0xFF707062], true), + .gradient([0xFFB09F99, 0xFF8F7E72], true), + .gradient([0xFFEBA15B, 0xFFA16730], true), + .gradient([0xFFE8B948, 0xFFB87C30], true), + .gradient([0xFF5E6F91, 0xFF415275], true), + .gradient([0xFF565D61, 0xFF3B4347], true), + .gradient([0xFF8F6655, 0xFF68443F], true), + .gradient([0xFF1B1B1B, 0xFF000000], true), + .gradient([0xFFAE72E3, 0xFF8854B5], true), + .gradient([0xFFC269BE, 0xFF8B4384], true), + .gradient([0xFF469CD3, 0xFF2E78A8], true), + .gradient([0xFF5BCEC5, 0xFF36928E], true), + .gradient([0xFF5FD66F, 0xFF319F76], true), + .gradient([0xFF66B27A, 0xFF33786D], true), + .gradient([0xFF6C9CF4, 0xFF5C6AEC], true), + .gradient([0xFFDA76A8, 0xFFAE5891], true), + .gradient([0xFFE66473, 0xFFA74559], true), + .gradient([0xFFAF75BC, 0xFF895196], true), + .gradient([0xFF438CB9, 0xFF2D6283], true), + .gradient([0xFF81B6B2, 0xFF4B9A96], true), + .gradient([0xFF66B27A, 0xFF33786D], true), + .gradient([0xFFCAB560, 0xFF8C803C], true), + .gradient([0xFFADB070, 0xFF6B7D54], true), + .gradient([0xFFBC7051, 0xFF975547], true), + .gradient([0xFFC7835E, 0xFF9E6345], true), + .gradient([0xFFE68A3C, 0xFFD45393], true), + .gradient([0xFF6BE2F2, 0xFF6675F7], true), + .gradient([0xFFC56DF4, 0xFF6073F4], true), + .gradient([0xFFEBC92F, 0xFF54B848], true) ] - case gradient([UInt32]) + case gradient([UInt32], Bool) public var colors: [UInt32] { switch self { - case let .gradient(colors): + case let .gradient(colors, _): return colors } } + public var isPremium: Bool { + switch self { + case let .gradient(_, isPremium): + return isPremium + } + } + public var isLight: Bool { switch self { - case let .gradient(colors): + case let .gradient(colors, _): if colors.count == 1 { return UIColor(rgb: colors.first!).lightness > 0.99 } else if colors.count == 2 { @@ -44,7 +82,7 @@ public enum AvatarBackground: Equatable { public func generateImage(size: CGSize) -> UIImage { switch self { - case let .gradient(colors): + case let .gradient(colors, _): if colors.count == 1 { return generateSingleColorImage(size: size, color: UIColor(rgb: colors.first!))! } else if colors.count == 2 { diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/BUILD b/submodules/TelegramUI/Components/AvatarEditorScreen/BUILD index 034b92f5ce..a87531995d 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/BUILD @@ -19,7 +19,7 @@ swift_library( "//submodules/Components/ViewControllerComponent:ViewControllerComponent", "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", "//submodules/Components/MultilineTextComponent:MultilineTextComponent", - "//submodules/Components/SolidRoundedButtonComponent", + "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/AccountContext:AccountContext", "//submodules/AppBundle:AppBundle", @@ -40,6 +40,8 @@ swift_library( "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", "//submodules/TelegramUI/Components/MediaEditor", "//submodules/TelegramUI/Components/AvatarBackground", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift index bfbcbd9d71..46a17c495e 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift @@ -20,11 +20,13 @@ import Markdown import GradientBackground import LegacyComponents import DrawingUI -import SolidRoundedButtonComponent +import ButtonComponent import AnimationCache import EmojiTextAttachmentView import MediaEditor import AvatarBackground +import LottieComponent +import UndoUI public struct AvatarKeyboardInputData: Equatable { var emoji: EmojiPagerContentComponent @@ -126,7 +128,14 @@ final class AvatarEditorScreenComponent: Component { }) } - self.selectedBackground = .gradient(markup.backgroundColors.map { UInt32(bitPattern: $0) }) + var isPremium = false + let colorsValue = markup.backgroundColors.map { UInt32(bitPattern: $0) } + if let defaultColor = AvatarBackground.defaultBackgrounds.first(where: { $0.colors == colorsValue}) { + if defaultColor.isPremium { + isPremium = true + } + } + self.selectedBackground = .gradient(colorsValue, isPremium) self.previousColor = self.selectedBackground } else { self.selectedBackground = AvatarBackground.defaultBackgrounds.first! @@ -188,6 +197,8 @@ final class AvatarEditorScreenComponent: Component { private let buttonView = ComponentView() private var component: AvatarEditorScreenComponent? + private var environment: EnvironmentType? + private weak var state: State? private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)? @@ -783,12 +794,15 @@ final class AvatarEditorScreenComponent: Component { private var isExpanded = false - func update(component: AvatarEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { + func update(component: AvatarEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component + + let environment = environment[EnvironmentType.self].value + self.environment = environment self.state = state - let environment = environment[ViewControllerComponentContainer.Environment.self].value let strings = environment.strings + let theme = environment.theme let controller = environment.controller self.controller = { @@ -990,6 +1004,7 @@ final class AvatarEditorScreenComponent: Component { transition: transition, component: AnyComponent(BackgroundColorComponent( theme: environment.theme, + isPremium: component.context.isPremium, values: AvatarBackground.defaultBackgrounds, selectedValue: state.selectedBackground, customValue: state.customColor, @@ -1037,8 +1052,8 @@ final class AvatarEditorScreenComponent: Component { colors: state.selectedBackground.colors, colorsChanged: { [weak state] colors in if let state { - state.customColor = .gradient(colors) - state.selectedBackground = .gradient(colors) + state.customColor = .gradient(colors, true) + state.selectedBackground = .gradient(colors, true) state.updated(transition: .immediate) } }, @@ -1268,29 +1283,65 @@ final class AvatarEditorScreenComponent: Component { case .suggest: buttonText = strings.AvatarEditor_SuggestProfilePhoto case .user: - buttonText = strings.AvatarEditor_SetProfilePhoto + //TODO:localize + buttonText = "Set My Photo" //strings.AvatarEditor_SetProfilePhoto case .group, .forum: buttonText = strings.AvatarEditor_SetGroupPhoto case .channel: buttonText = strings.AvatarEditor_SetChannelPhoto } + var isLocked = false + if component.peerType != .suggest, !component.context.isPremium { + if state.selectedBackground.isPremium { + isLocked = true + } + if let selectedFile = state.selectedFile { + if selectedFile.isSticker { + isLocked = true + } + } + } + + var buttonContents: [AnyComponentWithIdentity] = [] + buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(buttonText), component: AnyComponent( + Text(text: buttonText, font: Font.semibold(17.0), color: theme.list.itemCheckColors.foregroundColor) + ))) + if !component.context.isPremium && isLocked { + buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "premium_unlock"), + color: theme.list.itemCheckColors.foregroundColor, + startingPosition: .begin, + size: CGSize(width: 30.0, height: 30.0), + loop: true + )))) + } + let buttonSize = self.buttonView.update( transition: transition, component: AnyComponent( - SolidRoundedButtonComponent( - title: buttonText, - theme: SolidRoundedButtonComponent.Theme(theme: environment.theme), - fontSize: 17.0, - height: 50.0, - cornerRadius: 10.0, + ButtonComponent( + background: ButtonComponent.Background( + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + HStack(buttonContents, spacing: 3.0) + )), + isEnabled: true, + displaysProgress: false, action: { [weak self] in - self?.complete() + if isLocked { + self?.presentPremiumToast() + } else { + self?.complete() + } } ) ), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: environment.navigationHeight - environment.statusBarHeight) + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) ) if let buttonView = self.buttonView.view { if buttonView.superview == nil { @@ -1298,10 +1349,41 @@ final class AvatarEditorScreenComponent: Component { } transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize)) } + + let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight - 4.0), size: CGSize(width: availableSize.width, height: availableSize.height - contentHeight + 4.0)) + if let controller = environment.controller(), !controller.automaticallyControlPresentationContextLayout { + let layout = ContainerViewLayout( + size: availableSize, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomPanelFrame.height, 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: transition.containedViewLayoutTransition) + } return availableSize } + private func presentPremiumToast() { + guard let environment = self.environment, let component = self.component, let parentController = environment.controller() else { + return + } + HapticFeedback().impact(.light) + + let controller = premiumAlertController( + context: component.context, + parentController: parentController, + text: environment.strings.AvatarEditor_PremiumNeeded_Background + ) + parentController.present(controller, in: .window(.root)) + } + private let queue = Queue() func complete() { guard let state = self.state, let file = state.selectedFile, let controller = self.controller?() else { @@ -1531,6 +1613,9 @@ public final class AvatarEditorScreen: ViewControllerComponentContainer { let componentReady = Promise() super.init(context: context, component: AvatarEditorScreenComponent(context: context, ready: componentReady, peerType: peerType, markup: markup), navigationBarAppearance: .transparent) + + self.automaticallyControlPresentationContextLayout = false + self.navigationPresentation = .modal self.readyValue.set(componentReady.get() |> timeout(0.3, queue: .mainQueue(), alternate: .single(true))) diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/BackgroundColorComponent.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/BackgroundColorComponent.swift index 43a84b6a12..cf3df43339 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/BackgroundColorComponent.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/BackgroundColorComponent.swift @@ -10,6 +10,7 @@ import AvatarBackground final class BackgroundColorComponent: Component { let theme: PresentationTheme + let isPremium: Bool let values: [AvatarBackground] let selectedValue: AvatarBackground let customValue: AvatarBackground? @@ -18,6 +19,7 @@ final class BackgroundColorComponent: Component { init( theme: PresentationTheme, + isPremium: Bool, values: [AvatarBackground], selectedValue: AvatarBackground, customValue: AvatarBackground?, @@ -25,6 +27,7 @@ final class BackgroundColorComponent: Component { openColorPicker: @escaping () -> Void ) { self.theme = theme + self.isPremium = isPremium self.values = values self.selectedValue = selectedValue self.customValue = customValue @@ -36,6 +39,9 @@ final class BackgroundColorComponent: Component { if lhs.theme !== rhs.theme { return false } + if lhs.isPremium != rhs.isPremium { + return false + } if lhs.values != rhs.values { return false } @@ -48,25 +54,45 @@ final class BackgroundColorComponent: Component { return true } - class View: UIView { - private var views: [Int: ComponentView] = [:] + class View: UIView, UIScrollViewDelegate { + private var views: [AnyHashable: ComponentView] = [:] + private var scrollView: UIScrollView private var component: BackgroundColorComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { - super.init(frame: frame) + self.scrollView = UIScrollView() + self.scrollView.contentInsetAdjustmentBehavior = .never + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.showsVerticalScrollIndicator = false + super.init(frame: frame) + self.clipsToBounds = true + + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.scrollView.disablesInteractiveTransitionGestureRecognizer = true } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - func update(component: BackgroundColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.component = component - self.state = state + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateScrolling(transition: .immediate) + } + + func updateScrolling(transition: ComponentTransition) { + guard let component = self.component else { + return + } + + let itemSize = CGSize(width: 30.0, height: 30.0) + let sideInset: CGFloat = 12.0 + let spacing: CGFloat = 13.0 var values: [(AvatarBackground?, Bool)] = component.values.map { ($0, false) } if let customValue = component.customValue { @@ -75,50 +101,97 @@ final class BackgroundColorComponent: Component { values.append((nil, true)) } - let itemSize = CGSize(width: 30.0, height: 30.0) - let sideInset: CGFloat = 12.0 - let height: CGFloat = 50.0 - let delta = floorToScreenPixels((availableSize.width - sideInset * 2.0 - CGFloat(values.count) * itemSize.width) / CGFloat(values.count - 1)) + let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0) + var validIds: [AnyHashable] = [] for i in 0 ..< values.count { - let view: ComponentView - if let current = self.views[i] { - view = current - } else { - view = ComponentView() - self.views[i] = view + let position: CGFloat = sideInset + (spacing + itemSize.width) * CGFloat(i) + let itemFrame = CGRect(origin: CGPoint(x: position, y: 10.0), size: itemSize) + var isVisible = false + if visibleBounds.intersects(itemFrame) { + isVisible = true } - - let itemSize = view.update( - transition: transition, - component: AnyComponent( - BackgroundSwatchComponent( - theme: component.theme, - background: values[i].0, - isCustom: values[i].1, - isSelected: component.selectedValue == values[i].0, - action: { - if let value = values[i].0, component.selectedValue != value { - component.updateValue(value) - } else if values[i].1 { - component.openColorPicker() - } - } - ) - ), - environment: {}, - containerSize: itemSize - ) - if let itemView = view.view { - if itemView.superview == nil { - self.addSubview(itemView) + if isVisible { + let itemId = AnyHashable(i) + validIds.append(itemId) + + let view: ComponentView + if let current = self.views[itemId] { + view = current + } else { + view = ComponentView() + self.views[itemId] = view } - let position: CGFloat = sideInset + (delta + itemSize.width) * CGFloat(i) - transition.setFrame(view: itemView, frame: CGRect(origin: CGPoint(x: position, y: 10.0), size: itemSize)) + let _ = view.update( + transition: transition, + component: AnyComponent( + BackgroundSwatchComponent( + theme: component.theme, + background: values[i].0, + isCustom: values[i].1, + isSelected: component.selectedValue == values[i].0, + isLocked: i >= 7 && !values[i].1, + action: { + if let value = values[i].0, component.selectedValue != value { + component.updateValue(value) + } else if values[i].1 { + component.openColorPicker() + } + } + ) + ), + environment: {}, + containerSize: itemSize + ) + if let itemView = view.view { + if itemView.superview == nil { + self.scrollView.addSubview(itemView) + } + transition.setFrame(view: itemView, frame: itemFrame) + } } } - return CGSize(width: availableSize.width, height: height) + + var removeIds: [AnyHashable] = [] + for (id, item) in self.views { + if !validIds.contains(id) { + removeIds.append(id) + if let itemView = item.view { + itemView.removeFromSuperview() + } + } + } + for id in removeIds { + self.views.removeValue(forKey: id) + } + } + + func update(component: BackgroundColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let height: CGFloat = 50.0 + let size = CGSize(width: availableSize.width, height: height) + let scrollFrame = CGRect(origin: .zero, size: size) + + let itemSize = CGSize(width: 30.0, height: 30.0) + let sideInset: CGFloat = 12.0 + let spacing: CGFloat = 13.0 + + let count = component.values.count + 1 + let contentSize = CGSize(width: sideInset * 2.0 + CGFloat(count) * itemSize.width + CGFloat(count - 1) * spacing, height: height) + + if self.scrollView.frame != scrollFrame { + self.scrollView.frame = scrollFrame + } + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + + self.updateScrolling(transition: .immediate) + + return size } } @@ -164,11 +237,22 @@ private func generateMoreIcon() -> UIImage? { }) } +private var lockIcon: UIImage? = { + let icon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/SmallLock"), color: .white) + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + if let icon, let cgImage = icon.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - icon.size.width) / 2.0), y: floorToScreenPixels((size.height - icon.size.height) / 2.0)), size: icon.size), byTiling: false) + } + }) +}() + final class BackgroundSwatchComponent: Component { let theme: PresentationTheme let background: AvatarBackground? let isCustom: Bool let isSelected: Bool + let isLocked: Bool let action: () -> Void init( @@ -176,17 +260,19 @@ final class BackgroundSwatchComponent: Component { background: AvatarBackground?, isCustom: Bool, isSelected: Bool, + isLocked: Bool, action: @escaping () -> Void ) { self.theme = theme self.background = background self.isCustom = isCustom self.isSelected = isSelected + self.isLocked = isLocked self.action = action } static func == (lhs: BackgroundSwatchComponent, rhs: BackgroundSwatchComponent) -> Bool { - return lhs.theme === rhs.theme && lhs.background == rhs.background && lhs.isCustom == rhs.isCustom && lhs.isSelected == rhs.isSelected + return lhs.theme === rhs.theme && lhs.background == rhs.background && lhs.isCustom == rhs.isCustom && lhs.isSelected == rhs.isSelected && lhs.isLocked == rhs.isLocked } final class View: UIButton { @@ -283,6 +369,8 @@ final class BackgroundSwatchComponent: Component { self.iconLayer.contents = generateAddIcon(color: component.theme.list.itemAccentColor)?.cgImage } } + } else if component.isLocked { + self.iconLayer.contents = lockIcon?.cgImage } else { self.iconLayer.contents = nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift index 80ab042fb2..f45e1ee84a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift @@ -1893,7 +1893,7 @@ public final class ChatEmptyNode: ASDisplayNode { node.layer.animateScale(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) } } - self.isUserInteractionEnabled = [.peerNearby, .greeting, .premiumRequired, .cloud].contains(contentType) + self.isUserInteractionEnabled = [.peerNearby, .greeting, .premiumRequired, .starsRequired, .cloud].contains(contentType) 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/ChatHistoryEntry/Sources/ChatHistoryEntry.swift b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift index 79be541f9a..8a1d0f0ad7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift +++ b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift @@ -41,12 +41,17 @@ public struct ChatMessageEntryAttributes: Equatable { } } +public enum ChatInfoData: Equatable { + case botInfo(title: String, text: String, photo: TelegramMediaImage?, video: TelegramMediaFile?) + case userInfo(title: String, registrationDate: String?, phoneCountry: String?, locationCountry: String?, groupsInCommon: [EnginePeer]) +} + public enum ChatHistoryEntry: Identifiable, Comparable { case MessageEntry(Message, ChatPresentationData, Bool, MessageHistoryEntryLocation?, ChatHistoryMessageSelection, ChatMessageEntryAttributes) case MessageGroupEntry(Int64, [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)], ChatPresentationData) case UnreadEntry(MessageIndex, ChatPresentationData) case ReplyCountEntry(MessageIndex, Bool, Int, ChatPresentationData) - case ChatInfoEntry(String, String, TelegramMediaImage?, TelegramMediaFile?, ChatPresentationData) + case ChatInfoEntry(ChatInfoData, ChatPresentationData) case SearchEntry(PresentationTheme, PresentationStrings) public var stableId: UInt64 { @@ -272,8 +277,8 @@ public enum ChatHistoryEntry: Identifiable, Comparable { } else { return false } - case let .ChatInfoEntry(lhsTitle, lhsText, lhsPhoto, lhsVideo, lhsPresentationData): - if case let .ChatInfoEntry(rhsTitle, rhsText, rhsPhoto, rhsVideo, rhsPresentationData) = rhs, lhsTitle == rhsTitle, lhsText == rhsText, lhsPhoto == rhsPhoto, lhsVideo == rhsVideo, lhsPresentationData === rhsPresentationData { + case let .ChatInfoEntry(lhsData, lhsPresentationData): + if case let .ChatInfoEntry(rhsData, rhsPresentationData) = rhs, lhsData == rhsData, lhsPresentationData === rhsPresentationData { return true } else { return false diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift index cab565a18b..38ba7c58ec 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift @@ -1050,7 +1050,7 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate, } public var toggleQuoteCollapse: ((NSRange) -> Void)? - + private let displayInternal: ChatInputTextInternal private let measureInternal: ChatInputTextInternal @@ -1111,6 +1111,10 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate, self.delegate = self + if #available(iOS 18.0, *) { + self.supportsAdaptiveImageGlyph = false + } + self.displayInternal.updateDisplayElements = { [weak self] in self?.updateTextElements() } @@ -1217,7 +1221,7 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate, @objc public func textViewDidChange(_ textView: UITextView) { self.selectionChangedForEditedText = true - + self.updateTextContainerInset() self.customDelegate?.chatInputTextNodeDidUpdateText() diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift index 0ab1eea659..657ab5dc3c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift @@ -22,8 +22,8 @@ import TextNodeWithEntities import ChatMessageBubbleContentNode import ChatMessageItemCommon -private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, accountPeerId: PeerId, forForumOverview: Bool) -> NSAttributedString? { - return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: EngineMessage(message), accountPeerId: accountPeerId, forChatList: false, forForumOverview: forForumOverview) +private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, messageCount: Int? = nil, accountPeerId: PeerId, forForumOverview: Bool) -> NSAttributedString? { + return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: EngineMessage(message), messageCount: messageCount, accountPeerId: accountPeerId, forChatList: false, forForumOverview: forForumOverview) } public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { @@ -160,7 +160,12 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage return { item, layoutConstants, _, _, _, _ in - let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center) + var isDetached = false + if let _ = item.message.paidStarsAttribute { + isDetached = true + } + + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center, isDetached: isDetached) let backgroundImage = PresentationResourcesChat.chatActionPhotoBackgroundImage(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty) @@ -170,7 +175,12 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { forForumOverview = true } - let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: item.message, accountPeerId: item.context.account.peerId, forForumOverview: forForumOverview) + var messageCount: Int = 1 + if case let .group(messages) = item.content { + messageCount = messages.count + } + + let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: item.message, messageCount: messageCount, accountPeerId: item.context.account.peerId, forForumOverview: forForumOverview) var image: TelegramMediaImage? var story: TelegramMediaStory? diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift index 28066a7428..18f785f044 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift @@ -62,7 +62,6 @@ private extension UIBezierPath { } private final class ChatMessageActionButtonNode: ASDisplayNode { - //private let backgroundBlurNode: NavigationBackgroundNode private var backgroundBlurView: PortalView? private var titleNode: TextNode? @@ -84,15 +83,11 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { private let accessibilityArea: AccessibilityAreaNode override init() { - //self.backgroundBlurNode = NavigationBackgroundNode(color: .clear) - //self.backgroundBlurNode.isUserInteractionEnabled = false - self.accessibilityArea = AccessibilityAreaNode() self.accessibilityArea.accessibilityTraits = .button super.init() - //self.addSubnode(self.backgroundBlurNode) self.addSubnode(self.accessibilityArea) self.accessibilityArea.activate = { [weak self] in diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/BUILD index 56a646c36b..3869cfb7c9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/BUILD @@ -21,6 +21,7 @@ swift_library( "//submodules/ChatMessageBackground", "//submodules/TelegramUI/Components/ChatControllerInteraction", "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", + "//submodules/TelegramUI/Components/Chat/ChatMessageItem", "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", ], visibility = [ diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift index a097d80a07..34c49278af 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift @@ -10,6 +10,7 @@ import AccountContext import ChatMessageBackground import ChatControllerInteraction import ChatHistoryEntry +import ChatMessageItem import ChatMessageItemCommon import SwiftSignalKit @@ -180,6 +181,7 @@ public final class ChatMessageBubbleContentItem { public let controllerInteraction: ChatControllerInteraction public let message: Message public let topMessage: Message + public let content: ChatMessageItemContent public let read: Bool public let chatLocation: ChatLocation public let presentationData: ChatPresentationData @@ -188,11 +190,12 @@ public final class ChatMessageBubbleContentItem { public let isItemPinned: Bool public let isItemEdited: Bool - public init(context: AccountContext, controllerInteraction: ChatControllerInteraction, message: Message, topMessage: Message, read: Bool, chatLocation: ChatLocation, presentationData: ChatPresentationData, associatedData: ChatMessageItemAssociatedData, attributes: ChatMessageEntryAttributes, isItemPinned: Bool, isItemEdited: Bool) { + public init(context: AccountContext, controllerInteraction: ChatControllerInteraction, message: Message, topMessage: Message, content: ChatMessageItemContent, read: Bool, chatLocation: ChatLocation, presentationData: ChatPresentationData, associatedData: ChatMessageItemAssociatedData, attributes: ChatMessageEntryAttributes, isItemPinned: Bool, isItemEdited: Bool) { self.context = context self.controllerInteraction = controllerInteraction self.message = message self.topMessage = topMessage + self.content = content self.read = read self.chatLocation = chatLocation self.presentationData = presentationData diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 0b1d54f9c4..11c5a8cc1b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -121,13 +121,18 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ let hideAllAdditionalInfo = item.presentationData.isPreview var hasSeparateCommentsButton = false - + + var addedPriceInfo = false + outer: for (message, itemAttributes) in item.content { for attribute in message.attributes { if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil { result.append((message, ChatMessageRestrictedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false break outer + } else if let _ = attribute as? PaidStarsMessageAttribute, !addedPriceInfo { + result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + addedPriceInfo = true } } @@ -1791,7 +1796,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI var addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode, Int?)]? for contentNodeItemValue in contentNodeMessagesAndClasses { let contentNodeItem = contentNodeItemValue as (message: Message, type: AnyClass, attributes: ChatMessageEntryAttributes, bubbleAttributes: BubbleItemAttributes) - + var found = false for currentNodeItemValue in currentContentClassesPropertiesAndLayouts { let currentNodeItem = currentNodeItemValue as (message: Message, type: AnyClass, supportsMosaic: Bool, index: Int?, currentLayout: (ChatMessageBubbleContentItem, ChatMessageItemLayoutConstants, ChatMessageBubblePreparePosition, Bool?, CGSize, CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void)))) @@ -1803,7 +1808,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } if !found { - let contentNode = (contentNodeItem.type as! ChatMessageBubbleContentNode.Type).init() + let contentNode = (contentNodeItem.type as! ChatMessageBubbleContentNode.Type).init() contentNode.index = contentNodeItem.bubbleAttributes.index contentPropertiesAndPrepareLayouts.append((contentNodeItem.message, contentNode.supportsMosaic, contentNodeItem.attributes, contentNodeItem.bubbleAttributes, contentNode.asyncLayoutContent())) if addedContentNodes == nil { @@ -2028,7 +2033,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI prepareContentPosition = .linear(top: topPosition, bottom: refinedBottomPosition) } - let contentItem = ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: message, topMessage: item.content.firstMessage, read: read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: attributes, isItemPinned: isItemPinned, isItemEdited: isItemEdited) + let contentItem = ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: message, topMessage: item.content.firstMessage, content: item.content, read: read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: attributes, isItemPinned: isItemPinned, isItemEdited: isItemEdited) var itemSelection: Bool? switch content { @@ -2066,22 +2071,24 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI contentPropertiesAndLayouts.append((unboundSize, properties, prepareContentPosition, bubbleAttributes, nodeLayout, needSeparateContainers && !bubbleAttributes.isAttachment ? message.stableId : nil, itemSelection)) - switch properties.hidesBackground { - case .never: - backgroundHiding = .never - case .emptyWallpaper: - if backgroundHiding == nil { - backgroundHiding = properties.hidesBackground - } - case .always: - backgroundHiding = .always - } + if !properties.isDetached { + switch properties.hidesBackground { + case .never: + backgroundHiding = .never + case .emptyWallpaper: + if backgroundHiding == nil { + backgroundHiding = properties.hidesBackground + } + case .always: + backgroundHiding = .always + } - switch properties.forceAlignment { + switch properties.forceAlignment { case .none: break case .center: alignment = .center + } } index += 1 @@ -2870,7 +2877,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI bottomBubbleAttributes = contentPropertiesAndLayouts[i + 1].3 } - if i == 0 { + if i == 0 || (i == 1 && contentPropertiesAndLayouts[0].1.isDetached) { topPosition = firstNodeTopPosition } else { topPosition = .Neighbour(topBubbleAttributes.isAttachment, topBubbleAttributes.neighborType, topBubbleAttributes.neighborSpacing) @@ -2933,15 +2940,20 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let mosaicRange = mosaicRange, mosaicRange.contains(i), let (framesAndPositions, size) = calculatedGroupFramesAndSize { let mosaicIndex = i - mosaicRange.lowerBound - if mosaicIndex == 0 && i == 0 { + if mosaicIndex == 0 && (i == 0 || (i == 1 && detachedContentNodesHeight > 0)) { if !headerSize.height.isZero { contentNodesHeight += 7.0 totalContentNodesHeight += 7.0 } } + var contentNodeOriginY = contentNodesHeight + if detachedContentNodesHeight > 0 { + contentNodeOriginY -= detachedContentNodesHeight - 4.0 + } + let (_, apply) = finalize(maxContentWidth) - let contentNodeFrame = framesAndPositions[mosaicIndex].0.offsetBy(dx: 0.0, dy: contentNodesHeight) + let contentNodeFrame = framesAndPositions[mosaicIndex].0.offsetBy(dx: 0.0, dy: contentNodeOriginY) contentNodeFramesPropertiesAndApply.append((contentNodeFrame, properties, true, apply)) if i == mosaicRange.upperBound - 1 { @@ -2950,7 +2962,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI contentNodesHeight += size.height totalContentNodesHeight += size.height - + mosaicStatusOrigin = contentNodeFrame.bottomRight } } else { @@ -2984,7 +2996,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI currentItemSelection = itemSelection } - let contentNodeOriginY = contentNodesHeight - detachedContentNodesHeight + var contentNodeOriginY = contentNodesHeight + if detachedContentNodesHeight > 0 { + contentNodeOriginY -= detachedContentNodesHeight - 4.0 + } + let (size, apply) = finalize(maxContentWidth) let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentNodeOriginY), size: size) contentNodeFramesPropertiesAndApply.append((containerFrame, properties, contentGroupId == nil, apply)) @@ -2998,7 +3014,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI totalContentNodesHeight += size.height if properties.isDetached { - detachedContentNodesHeight += size.height + detachedContentNodesHeight += size.height + 4.0 + totalContentNodesHeight += 4.0 } } } @@ -3160,7 +3177,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI nameNodeSizeApply: nameNodeSizeApply, viaWidth: viaWidth, contentOrigin: contentOrigin, - nameNodeOriginY: nameNodeOriginY, + nameNodeOriginY: nameNodeOriginY + detachedContentNodesHeight, authorNameColor: authorNameColor, layoutConstants: layoutConstants, currentCredibilityIcon: currentCredibilityIcon, @@ -3168,11 +3185,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI boostNodeSizeApply: boostNodeSizeApply, contentUpperRightCorner: contentUpperRightCorner, threadInfoSizeApply: threadInfoSizeApply, - threadInfoOriginY: threadInfoOriginY, + threadInfoOriginY: threadInfoOriginY + detachedContentNodesHeight, forwardInfoSizeApply: forwardInfoSizeApply, - forwardInfoOriginY: forwardInfoOriginY, + forwardInfoOriginY: forwardInfoOriginY + detachedContentNodesHeight, replyInfoSizeApply: replyInfoSizeApply, - replyInfoOriginY: replyInfoOriginY, + replyInfoOriginY: replyInfoOriginY + detachedContentNodesHeight, removedContentNodeIndices: removedContentNodeIndices, updatedContentNodeOrder: updatedContentNodeOrder, addedContentNodes: addedContentNodes, @@ -4049,32 +4066,33 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } if let addedContentNodes = addedContentNodes { - for (contentNodeMessage, isAttachment, contentNode, _ ) in addedContentNodes { + for (contentNodeMessage, isAttachment, contentNode, _) in addedContentNodes { + let index = updatedContentNodes.count updatedContentNodes.append(contentNode) - let contextSourceNode: ContextExtractedContentContainingNode - let containerSupernode: ASDisplayNode - if isAttachment { - contextSourceNode = strongSelf.mainContextSourceNode - containerSupernode = strongSelf.clippingNode + if index < contentNodeFramesPropertiesAndApply.count && contentNodeFramesPropertiesAndApply[index].1.isDetached { + strongSelf.addSubnode(contentNode) } else { - contextSourceNode = strongSelf.contentContainers.first(where: { $0.contentMessageStableId == contentNodeMessage.stableId })?.sourceNode ?? strongSelf.mainContextSourceNode - containerSupernode = strongSelf.contentContainers.first(where: { $0.contentMessageStableId == contentNodeMessage.stableId })?.sourceNode.contentNode ?? strongSelf.clippingNode + let contextSourceNode: ContextExtractedContentContainingNode + let containerSupernode: ASDisplayNode + if isAttachment { + contextSourceNode = strongSelf.mainContextSourceNode + containerSupernode = strongSelf.clippingNode + } else { + contextSourceNode = strongSelf.contentContainers.first(where: { $0.contentMessageStableId == contentNodeMessage.stableId })?.sourceNode ?? strongSelf.mainContextSourceNode + containerSupernode = strongSelf.contentContainers.first(where: { $0.contentMessageStableId == contentNodeMessage.stableId })?.sourceNode.contentNode ?? strongSelf.clippingNode + } + containerSupernode.addSubnode(contentNode) + contentNode.updateIsTextSelectionActive = { [weak contextSourceNode] value in + contextSourceNode?.updateDistractionFreeMode?(value) + } + contentNode.updateIsExtractedToContextPreview(contextSourceNode.isExtractedToContextPreview) } - #if DEBUG && false - contentNode.layer.borderColor = UIColor(white: 0.0, alpha: 0.2).cgColor - contentNode.layer.borderWidth = 1.0 - #endif - - containerSupernode.addSubnode(contentNode) - contentNode.itemNode = strongSelf contentNode.bubbleBackgroundNode = strongSelf.backgroundNode contentNode.bubbleBackdropNode = strongSelf.backgroundWallpaperNode - contentNode.updateIsTextSelectionActive = { [weak contextSourceNode] value in - contextSourceNode?.updateDistractionFreeMode?(value) - } + contentNode.requestInlineUpdate = { [weak strongSelf] in guard let strongSelf else { return @@ -4082,7 +4100,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI strongSelf.internalUpdateLayout() } - contentNode.updateIsExtractedToContextPreview(contextSourceNode.isExtractedToContextPreview) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode/Sources/ChatMessageInstantVideoBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode/Sources/ChatMessageInstantVideoBubbleContentNode.swift index 8a1e689408..b7b1f1fd10 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode/Sources/ChatMessageInstantVideoBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode/Sources/ChatMessageInstantVideoBubbleContentNode.swift @@ -273,7 +273,7 @@ public class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentN let leftInset: CGFloat = 0.0 let rightInset: CGFloat = 0.0 - let (videoLayout, videoApply) = interactiveVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.message, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.attributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), constrainedSize.width - leftInset - rightInset - avatarInset, displaySize, maximumDisplaySize, isPlaying ? 1.0 : 0.0, .free, automaticDownload, avatarInset) + let (videoLayout, videoApply) = interactiveVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.message, content: item.content, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.attributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), constrainedSize.width - leftInset - rightInset - avatarInset, displaySize, maximumDisplaySize, isPlaying ? 1.0 : 0.0, .free, automaticDownload, avatarInset) let videoFrame = CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: videoLayout.contentSize) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index f25307df04..2bbaf56794 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -423,7 +423,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco isReplyThread = true } - let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.content.firstMessage, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, maximumDisplaySize, isPlaying ? 1.0 : 0.0, .free, automaticDownload, 0.0) + let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.content.firstMessage, content: item.content, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, maximumDisplaySize, isPlaying ? 1.0 : 0.0, .free, automaticDownload, 0.0) let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + effectiveAvatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - videoLayout.contentSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: 0.0), size: videoLayout.contentSize) @@ -1373,7 +1373,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco effectiveAvatarInset *= (1.0 - scaleProgress) displaySize = CGSize(width: initialSize.width + (targetSize.width - initialSize.width) * animationProgress, height: initialSize.height + (targetSize.height - initialSize.height) * animationProgress) - let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.message, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, maximumDisplaySize, scaleProgress, .free, self.appliedAutomaticDownload, 0.0) + let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.message, content: item.content, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, maximumDisplaySize, scaleProgress, .free, self.appliedAutomaticDownload, 0.0) let availableContentWidth = params.width - params.leftInset - params.rightInset - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + effectiveAvatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - videoLayout.contentSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: 0.0), size: videoLayout.contentSize) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index b603ac77f1..5946804836 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -2174,6 +2174,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr GiftItemComponent( context: context, theme: presentationData.theme.theme, + strings: presentationData.strings, subject: .uniqueGift(gift: gift), mode: .preview ) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index 28ba342aaa..bc45a37839 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -87,6 +87,11 @@ private func messagesShouldBeMerged(accountPeerId: PeerId, _ lhs: Message, _ rhs sameChat = false } + var isPaid = false + if let _ = lhs.paidStarsAttribute, let _ = rhs.paidStarsAttribute { + isPaid = true + } + var sameThread = true if let lhsPeer = lhs.peers[lhs.id.peerId], let rhsPeer = rhs.peers[rhs.id.peerId], arePeersEqual(lhsPeer, rhsPeer), let channel = lhsPeer as? TelegramChannel, channel.flags.contains(.isForum), lhs.threadId != rhs.threadId { sameThread = false @@ -136,7 +141,7 @@ private func messagesShouldBeMerged(accountPeerId: PeerId, _ lhs: Message, _ rhs } } - if abs(lhsEffectiveTimestamp - rhsEffectiveTimestamp) < Int32(10 * 60) && sameChat && sameAuthor && sameThread { + if abs(lhsEffectiveTimestamp - rhsEffectiveTimestamp) < Int32(10 * 60) && sameChat && sameAuthor && sameThread && !isPaid { if let channel = lhs.peers[lhs.id.peerId] as? TelegramChannel, case .group = channel.info, lhsEffectiveAuthor?.id == channel.id, !lhs.effectivelyIncoming(accountPeerId) { return .none } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/BUILD new file mode 100644 index 0000000000..ffec32b87e --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/BUILD @@ -0,0 +1,33 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessagePaymentAlertController", + module_name = "ChatMessagePaymentAlertController", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/AccountContext:AccountContext", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/ComponentFlow", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/TextFormat", + "//submodules/AvatarNode", + "//submodules/CheckNode", + "//submodules/TelegramUIPreferences", + "//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent", + "//submodules/Markdown", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessagePaymentAlertController.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift similarity index 77% rename from submodules/TelegramUI/Sources/Chat/ChatMessagePaymentAlertController.swift rename to submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift index 3834ff6747..ec77b0a7c1 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessagePaymentAlertController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift @@ -55,7 +55,7 @@ private final class ChatMessagePaymentAlertContentNode: AlertContentNode, ASGest var openTerms: () -> Void = {} - init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, optionText: String?, actions: [TextAlertAction], alignment: TextAlertContentActionLayout) { + init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, optionText: String?, actions: [TextAlertAction], alignment: TextAlertContentActionLayout) { self.strings = strings self.title = title self.text = text @@ -318,94 +318,143 @@ private final class ChatMessagePaymentAlertContentNode: AlertContentNode, ASGest } private class ChatMessagePaymentAlertController: AlertController { - private let context: AccountContext + private let context: AccountContext? private let presentationData: PresentationData + private weak var parentNavigationController: NavigationController? private let balance = ComponentView() - init(context: AccountContext, presentationData: PresentationData, contentNode: AlertContentNode) { + init(context: AccountContext?, presentationData: PresentationData, contentNode: AlertContentNode, navigationController: NavigationController?) { self.context = context self.presentationData = presentationData - + self.parentNavigationController = navigationController + super.init(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) + + self.willDismiss = { [weak self] in + guard let self else { + return + } + self.animateOut() + } } required public init(coder aDecoder: NSCoder) { preconditionFailure() } - override func dismissAnimated() { - super.dismissAnimated() - + private func animateOut() { if let view = self.balance.view { view.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4, removeOnCompletion: false) view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) } } + override func dismissAnimated() { + super.dismissAnimated() + + self.animateOut() + } + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - let insets = layout.insets(options: .statusBar) - let balanceSize = self.balance.update( - transition: .immediate, - component: AnyComponent( - StarsBalanceOverlayComponent( - context: self.context, - theme: self.presentationData.theme, - action: { - - } - ) - ), - environment: {}, - containerSize: layout.size - ) - if let view = self.balance.view { - if view.superview == nil { - self.view.addSubview(view) - - view.layer.animatePosition(from: CGPoint(x: 0.0, y: -64.0), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - view.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, removeOnCompletion: true, additive: false, completion: nil) - view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + if let context = self.context, let _ = self.parentNavigationController { + let insets = layout.insets(options: .statusBar) + let balanceSize = self.balance.update( + transition: .immediate, + component: AnyComponent( + StarsBalanceOverlayComponent( + context: context, + theme: self.presentationData.theme, + action: { [weak self] in + guard let self, let starsContext = context.starsContext, let navigationController = self.parentNavigationController 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) + }) + } + ) + ), + environment: {}, + containerSize: layout.size + ) + if let view = self.balance.view { + if view.superview == nil { + self.view.addSubview(view) + + view.layer.animatePosition(from: CGPoint(x: 0.0, y: -64.0), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + view.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, removeOnCompletion: true, additive: false, completion: nil) + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - balanceSize.width) / 2.0), y: insets.top + 5.0), size: balanceSize) } - view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - balanceSize.width) / 2.0), y: insets.top + 5.0), size: balanceSize) } } } public func chatMessagePaymentAlertController( - context: AccountContext, + context: AccountContext?, + presentationData: PresentationData, updatedPresentationData: (initial: PresentationData, signal: Signal)?, - peer: EnginePeer, + peers: [EnginePeer], + count: Int32, amount: StarsAmount, + totalAmount: StarsAmount?, + hasCheck: Bool = true, + navigationController: NavigationController?, completion: @escaping (Bool) -> Void ) -> AlertController { let theme = defaultDarkColorPresentationTheme - let presentationData: PresentationData - if let updatedPresentationData { - presentationData = updatedPresentationData.initial - } else { - presentationData = context.sharedContext.currentPresentationData.with { $0 } - } + let presentationData = updatedPresentationData?.initial ?? presentationData let strings = presentationData.strings var completionImpl: (() -> Void)? var dismissImpl: (() -> Void)? //TODO:localize - let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: "Pay for 1 Message", action: { + let title = "Confirm Payment" + let actionTitle: String + let messagesString: String + if count > 1 { + messagesString = "**\(count)** messages" + actionTitle = "Pay for \(count) Messages" + } else { + messagesString = "**\(count)** message" + actionTitle = "Pay for 1 Message" + } + + let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: actionTitle, action: { completionImpl?() dismissImpl?() }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { dismissImpl?() })] - let title = "Confirm Payment" - let text = "**\(peer.compactDisplayTitle)** charges **\(amount.value) Stars** per incoming message. Would you like to pay **\(amount.value) Stars** to send one message?" - let optionText = "Don't ask again" + + let text: String + if peers.count == 1, let peer = peers.first { + text = "**\(peer.compactDisplayTitle)** charges **\(amount.value) Stars** per incoming message. Would you like to pay **\(amount.value * Int64(count)) Stars** to send \(messagesString)?" + } else { + let amount = totalAmount ?? amount + text = "You selected **\(peers.count)** users who charge Stars for messages. Would you like to pay **\(amount.value)** Stars to send \(messagesString)?" + } - let contentNode = ChatMessagePaymentAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, optionText: optionText, actions: actions, alignment: .vertical) + let optionText = hasCheck ? "Don't ask again" : nil + + let contentNode = ChatMessagePaymentAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, optionText: optionText, actions: actions, alignment: .vertical) completionImpl = { [weak contentNode] in guard let contentNode else { @@ -414,7 +463,7 @@ public func chatMessagePaymentAlertController( completion(contentNode.dontAskAgain) } - let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode) + let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode, navigationController: navigationController) dismissImpl = { [weak controller] in controller?.dismissAnimated() } @@ -424,19 +473,16 @@ public func chatMessagePaymentAlertController( public func chatMessageRemovePaymentAlertController( - context: AccountContext, + context: AccountContext? = nil, + presentationData: PresentationData, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, amount: StarsAmount?, + navigationController: NavigationController?, completion: @escaping (Bool) -> Void ) -> AlertController { let theme = defaultDarkColorPresentationTheme - let presentationData: PresentationData - if let updatedPresentationData { - presentationData = updatedPresentationData.initial - } else { - presentationData = context.sharedContext.currentPresentationData.with { $0 } - } + let presentationData = updatedPresentationData?.initial ?? presentationData let strings = presentationData.strings var completionImpl: (() -> Void)? @@ -457,7 +503,7 @@ public func chatMessageRemovePaymentAlertController( let text = "Are you sure you want to allow **\(peer.compactDisplayTitle)** to message you for free?" let optionText = amount.flatMap { "Refund already paid **\($0.value) Stars**" } - let contentNode = ChatMessagePaymentAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, optionText: optionText, actions: actions, alignment: .horizontal) + let contentNode = ChatMessagePaymentAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, optionText: optionText, actions: actions, alignment: .horizontal) completionImpl = { [weak contentNode] in guard let contentNode else { @@ -466,7 +512,7 @@ public func chatMessageRemovePaymentAlertController( completion(contentNode.dontAskAgain) } - let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode) + let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode, navigationController: navigationController) dismissImpl = { [weak controller] in controller?.dismissAnimated() } diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index cf8db29d6c..7da3744cef 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -523,6 +523,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess controllerInteraction: controllerInteraction, message: message, topMessage: message, + content: .message(message: message, read: true, selection: .none, attributes: entryAttributes, location: nil), read: true, chatLocation: .peer(id: self.context.account.peerId), presentationData: chatPresentationData, diff --git a/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/BUILD b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/BUILD new file mode 100644 index 0000000000..aa10d348c0 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/BUILD @@ -0,0 +1,34 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatUserInfoItem", + module_name = "ChatUserInfoItem", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TextFormat", + "//submodules/UrlEscaping", + "//submodules/PhotoResources", + "//submodules/AccountContext", + "//submodules/MediaPlayer:UniversalMediaPlayer", + "//submodules/TelegramUniversalVideoContent", + "//submodules/WallpaperBackgroundNode", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/CountrySelectionUI", + "//submodules/TelegramStringFormatting", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift new file mode 100644 index 0000000000..ef8946b103 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift @@ -0,0 +1,589 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TextFormat +import UrlEscaping +import PhotoResources +import AccountContext +import UniversalMediaPlayer +import TelegramUniversalVideoContent +import WallpaperBackgroundNode +import ChatControllerInteraction +import ChatMessageBubbleContentNode +import CountrySelectionUI +import TelegramStringFormatting + +public final class ChatUserInfoItem: ListViewItem { + fileprivate let title: String + fileprivate let registrationDate: String? + fileprivate let phoneCountry: String? + fileprivate let locationCountry: String? + fileprivate let groupsInCommon: [EnginePeer] + fileprivate let controllerInteraction: ChatControllerInteraction + fileprivate let presentationData: ChatPresentationData + fileprivate let context: AccountContext + + public init( + title: String, + registrationDate: String?, + phoneCountry: String?, + locationCountry: String?, + groupsInCommon: [EnginePeer], + controllerInteraction: ChatControllerInteraction, + presentationData: ChatPresentationData, + context: AccountContext + ) { + self.title = title + self.registrationDate = registrationDate + self.phoneCountry = phoneCountry + self.locationCountry = locationCountry + self.groupsInCommon = groupsInCommon + self.controllerInteraction = controllerInteraction + self.presentationData = presentationData + self.context = context + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + let configure = { + let node = ChatUserInfoItemNode() + + let nodeLayout = node.asyncLayout() + let (layout, apply) = nodeLayout(self, params) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply(.None) }) + }) + } + } + if Thread.isMainThread { + async { + configure() + } + } else { + configure() + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? ChatUserInfoItemNode { + let nodeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = nodeLayout(self, params) + Queue.mainQueue().async { + completion(layout, { _ in + apply(animation) + }) + } + } + } + } + } +} + +public final class ChatUserInfoItemNode: ListViewItemNode { + public var controllerInteraction: ChatControllerInteraction? + + public let offsetContainer: ASDisplayNode + public let titleNode: TextNode + public let subtitleNode: TextNode + + private let registrationDateTitleTextNode: TextNode + private let registrationDateValueTextNode: TextNode + private var registrationDateText: String? + + private let phoneCountryTitleTextNode: TextNode + private let phoneCountryValueTextNode: TextNode + private var phoneCountryText: String? + + private let locationCountryTitleTextNode: TextNode + private let locationCountryValueTextNode: TextNode + private var locationCountryText: String? + + private let groupsTextNode: TextNode + + private var theme: ChatPresentationThemeData? + + private var wallpaperBackgroundNode: WallpaperBackgroundNode? + private var backgroundContent: WallpaperBubbleBackgroundNode? + + private var absolutePosition: (CGRect, CGSize)? + + private var item: ChatUserInfoItem? + + public init() { + self.offsetContainer = ASDisplayNode() + + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + self.titleNode.displaysAsynchronously = false + + self.subtitleNode = TextNode() + self.subtitleNode.isUserInteractionEnabled = false + self.subtitleNode.displaysAsynchronously = false + + self.registrationDateTitleTextNode = TextNode() + self.registrationDateTitleTextNode.isUserInteractionEnabled = false + self.registrationDateTitleTextNode.displaysAsynchronously = false + self.registrationDateValueTextNode = TextNode() + self.registrationDateValueTextNode.isUserInteractionEnabled = false + self.registrationDateValueTextNode.displaysAsynchronously = false + + self.phoneCountryTitleTextNode = TextNode() + self.phoneCountryTitleTextNode.isUserInteractionEnabled = false + self.phoneCountryTitleTextNode.displaysAsynchronously = false + self.phoneCountryValueTextNode = TextNode() + self.phoneCountryValueTextNode.isUserInteractionEnabled = false + self.phoneCountryValueTextNode.displaysAsynchronously = false + + self.locationCountryTitleTextNode = TextNode() + self.locationCountryTitleTextNode.isUserInteractionEnabled = false + self.locationCountryTitleTextNode.displaysAsynchronously = false + self.locationCountryValueTextNode = TextNode() + self.locationCountryValueTextNode.isUserInteractionEnabled = false + self.locationCountryValueTextNode.displaysAsynchronously = false + + self.groupsTextNode = TextNode() + self.groupsTextNode.isUserInteractionEnabled = false + self.groupsTextNode.displaysAsynchronously = false + + super.init(layerBacked: false, dynamicBounce: true, rotated: true) + + self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + + self.addSubnode(self.offsetContainer) + self.offsetContainer.addSubnode(self.titleNode) + self.offsetContainer.addSubnode(self.subtitleNode) + self.offsetContainer.addSubnode(self.groupsTextNode) + self.wantsTrailingItemSpaceUpdates = true + } + + override public func didLoad() { + super.didLoad() + +// let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) +// recognizer.tapActionAtPoint = { [weak self] point in +// if let strongSelf = self { +// let tapAction = strongSelf.tapActionAtPoint(point, gesture: .tap, isEstimating: true) +// switch tapAction.content { +// case .none: +// break +// case .ignore: +// return .fail +// case .url, .phone, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji, .custom: +// return .waitForSingleTap +// } +// } +// +// return .waitForDoubleTap +// } +// recognizer.highlight = { [weak self] point in +// if let strongSelf = self { +// strongSelf.updateTouchesAtPoint(point) +// } +// } +// self.view.addGestureRecognizer(recognizer) + } + + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + super.updateAbsoluteRect(rect, within: containerSize) + + self.absolutePosition = (rect, containerSize) + if let backgroundContent = self.backgroundContent { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += containerSize.height - rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) + } + } + + public func asyncLayout() -> (_ item: ChatUserInfoItem, _ width: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) + let makeRegistrationDateTitleLayout = TextNode.asyncLayout(self.registrationDateTitleTextNode) + let makeRegistrationDateValueLayout = TextNode.asyncLayout(self.registrationDateValueTextNode) + let makePhoneCountryTitleLayout = TextNode.asyncLayout(self.phoneCountryTitleTextNode) + let makePhoneCountryValueLayout = TextNode.asyncLayout(self.phoneCountryValueTextNode) + let makeLocationCountryTitleLayout = TextNode.asyncLayout(self.locationCountryTitleTextNode) + let makeLocationCountryValueLayout = TextNode.asyncLayout(self.locationCountryValueTextNode) + let makeGroupsLayout = TextNode.asyncLayout(self.groupsTextNode) + + let currentRegistrationDateText = self.registrationDateText + let currentPhoneCountryText = self.phoneCountryText + let currentLocationCountryText = self.locationCountryText + + return { [weak self] item, params in + self?.item = item + + var backgroundSize = CGSize(width: 240.0, height: 0.0) + + let verticalItemInset: CGFloat = 10.0 + let horizontalInset: CGFloat = 10.0 + params.leftInset + let horizontalContentInset: CGFloat = 16.0 + let verticalInset: CGFloat = 17.0 + let verticalSpacing: CGFloat = 6.0 + let paragraphSpacing: CGFloat = 3.0 + let attributeSpacing: CGFloat = 10.0 + + let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText + let subtitleColor = primaryTextColor.withAlphaComponent(item.presentationData.theme.theme.overallDarkAppearance ? 0.7 : 0.8) + + backgroundSize.height += verticalInset + //TODO:localize + let constrainedWidth = params.width - (horizontalInset + horizontalContentInset) * 2.0 + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: Font.semibold(15.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + backgroundSize.height += titleLayout.size.height + backgroundSize.height += verticalSpacing + + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Not a contact", font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + backgroundSize.height += subtitleLayout.size.height + backgroundSize.height += verticalSpacing + paragraphSpacing + + let infoConstrainedSize = CGSize(width: constrainedWidth * 0.7, height: CGFloat.greatestFiniteMagnitude) + + var maxTitleWidth: CGFloat = 0.0 + var maxValueWidth: CGFloat = 0.0 + + var registrationDateText: String? + let registrationDateTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)? + let registrationDateValueLayoutAndApply: (TextNodeLayout, () -> TextNode)? + if let registrationDate = item.registrationDate { + if let currentRegistrationDateText { + registrationDateText = currentRegistrationDateText + } else { + let components = registrationDate.components(separatedBy: ".") + if components.count == 2, let first = Int32(components[0]), let second = Int32(components[1]) { + let month = first - 1 + let year = second - 1900 + registrationDateText = stringForMonth(strings: item.presentationData.strings, month: month, ofYear: year) + } else { + registrationDateText = "" + } + } + registrationDateTitleLayoutAndApply = makeRegistrationDateTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Registration", font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) + registrationDateValueLayoutAndApply = makeRegistrationDateValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: registrationDateText ?? "", font: Font.semibold(13.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) + backgroundSize.height += verticalSpacing + backgroundSize.height += registrationDateValueLayoutAndApply?.0.size.height ?? 0 + + maxTitleWidth = max(maxTitleWidth, (registrationDateTitleLayoutAndApply?.0.size.width ?? 0)) + maxValueWidth = max(maxValueWidth, (registrationDateValueLayoutAndApply?.0.size.width ?? 0)) + } else { + registrationDateTitleLayoutAndApply = nil + registrationDateValueLayoutAndApply = nil + } + + var phoneCountryText: String? + let phoneCountryTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)? + let phoneCountryValueLayoutAndApply: (TextNodeLayout, () -> TextNode)? + if let phoneCountry = item.phoneCountry { + if let currentPhoneCountryText { + phoneCountryText = currentPhoneCountryText + } 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 + } + phoneCountryText = emojiFlagForISOCountryCode(phoneCountry) + " " + countryName + } + phoneCountryTitleLayoutAndApply = makePhoneCountryTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Phone Number", font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) + phoneCountryValueLayoutAndApply = makePhoneCountryValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: phoneCountryText ?? "", font: Font.semibold(13.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) + backgroundSize.height += verticalSpacing + backgroundSize.height += phoneCountryValueLayoutAndApply?.0.size.height ?? 0 + + maxTitleWidth = max(maxTitleWidth, (phoneCountryTitleLayoutAndApply?.0.size.width ?? 0)) + maxValueWidth = max(maxValueWidth, (phoneCountryValueLayoutAndApply?.0.size.width ?? 0)) + } else { + phoneCountryTitleLayoutAndApply = nil + phoneCountryValueLayoutAndApply = nil + } + + var locationCountryText: String? + let locationCountryTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)? + let locationCountryValueLayoutAndApply: (TextNodeLayout, () -> TextNode)? + if let locationCountry = item.locationCountry { + if let currentLocationCountryText { + locationCountryText = currentLocationCountryText + } else { + var countryName = "" + let countriesConfiguration = item.context.currentCountriesConfiguration.with { $0 } + if let country = countriesConfiguration.countries.first(where: { $0.id == locationCountry }) { + countryName = country.localizedName ?? country.name + } + locationCountryText = emojiFlagForISOCountryCode(locationCountry) + " " + countryName + } + locationCountryTitleLayoutAndApply = makeLocationCountryTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Location", font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) + locationCountryValueLayoutAndApply = makeLocationCountryValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: locationCountryText ?? "", font: Font.semibold(13.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) + backgroundSize.height += verticalSpacing + backgroundSize.height += locationCountryValueLayoutAndApply?.0.size.height ?? 0 + + maxTitleWidth = max(maxTitleWidth, (locationCountryTitleLayoutAndApply?.0.size.width ?? 0)) + maxValueWidth = max(maxValueWidth, (locationCountryValueLayoutAndApply?.0.size.width ?? 0)) + } else { + locationCountryTitleLayoutAndApply = nil + locationCountryValueLayoutAndApply = nil + } + + backgroundSize.width = horizontalContentInset * 3.0 + maxTitleWidth + attributeSpacing + maxValueWidth + + let (groupsLayout, groupsApply) = makeGroupsLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "No groups in common", font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + backgroundSize.height += verticalSpacing * 2.0 + paragraphSpacing + backgroundSize.height += groupsLayout.size.height + + backgroundSize.height += verticalInset + + let backgroundFrame = CGRect(origin: CGPoint(x: floor((params.width - backgroundSize.width) / 2.0), y: verticalItemInset + 4.0), size: backgroundSize) + + let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: backgroundSize.height + verticalItemInset * 2.0), insets: UIEdgeInsets()) + return (itemLayout, { _ in + if let strongSelf = self { + strongSelf.theme = item.presentationData.theme + + if item.presentationData.theme.theme.overallDarkAppearance { + strongSelf.registrationDateTitleTextNode.layer.compositingFilter = nil + strongSelf.phoneCountryTitleTextNode.layer.compositingFilter = nil + strongSelf.locationCountryTitleTextNode.layer.compositingFilter = nil + strongSelf.subtitleNode.layer.compositingFilter = nil + strongSelf.groupsTextNode.layer.compositingFilter = nil + } else { + strongSelf.registrationDateTitleTextNode.layer.compositingFilter = "overlayBlendMode" + strongSelf.phoneCountryTitleTextNode.layer.compositingFilter = "overlayBlendMode" + strongSelf.locationCountryTitleTextNode.layer.compositingFilter = "overlayBlendMode" + strongSelf.subtitleNode.layer.compositingFilter = "overlayBlendMode" + strongSelf.groupsTextNode.layer.compositingFilter = "overlayBlendMode" + } + + strongSelf.registrationDateText = registrationDateText + strongSelf.phoneCountryText = phoneCountryText + strongSelf.locationCountryText = locationCountryText + + strongSelf.controllerInteraction = item.controllerInteraction + + strongSelf.offsetContainer.frame = CGRect(origin: CGPoint(), size: itemLayout.contentSize) + + let _ = titleApply() + var contentOriginY = backgroundFrame.origin.y + verticalInset + let titleFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + floor((backgroundSize.width - titleLayout.size.width) / 2.0), y: contentOriginY), size: titleLayout.size) + strongSelf.titleNode.frame = titleFrame + contentOriginY += titleLayout.size.height + contentOriginY += verticalSpacing - paragraphSpacing + + let _ = subtitleApply() + let subtitleFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + floor((backgroundSize.width - subtitleLayout.size.width) / 2.0), y: contentOriginY), size: subtitleLayout.size) + strongSelf.subtitleNode.frame = subtitleFrame + contentOriginY += subtitleLayout.size.height + contentOriginY += verticalSpacing * 2.0 + paragraphSpacing + + var attributeMidpoints: [CGFloat] = [] + + func appendAttributeMidpoint(titleLayout: TextNodeLayout?, valueLayout: TextNodeLayout?) { + if let titleLayout, let valueLayout { + let totalWidth = titleLayout.size.width + attributeSpacing + valueLayout.size.width + let titleOffset = titleLayout.size.width + attributeSpacing / 2.0 + let midpoint = (backgroundSize.width - totalWidth) / 2.0 + titleOffset + attributeMidpoints.append(midpoint) + } + } + appendAttributeMidpoint(titleLayout: registrationDateTitleLayoutAndApply?.0, valueLayout: registrationDateValueLayoutAndApply?.0) + appendAttributeMidpoint(titleLayout: phoneCountryTitleLayoutAndApply?.0, valueLayout: phoneCountryValueLayoutAndApply?.0) + appendAttributeMidpoint(titleLayout: locationCountryTitleLayoutAndApply?.0, valueLayout: locationCountryValueLayoutAndApply?.0) + + let middleX = floorToScreenPixels(attributeMidpoints.isEmpty ? backgroundSize.width / 2.0 : attributeMidpoints.reduce(0, +) / CGFloat(attributeMidpoints.count)) + + let titleMaxX: CGFloat = backgroundFrame.minX + middleX - attributeSpacing / 2.0 + let valueMinX: CGFloat = backgroundFrame.minX + middleX + attributeSpacing / 2.0 + + func positionAttributeNodes( + titleTextNode: TextNode, + valueTextNode: TextNode, + titleLayoutAndApply: (TextNodeLayout, () -> TextNode)?, + valueLayoutAndApply: (TextNodeLayout, () -> TextNode)? + ) { + if let (titleLayout, titleApply) = titleLayoutAndApply { + if titleTextNode.supernode == nil { + strongSelf.offsetContainer.addSubnode(titleTextNode) + } + let _ = titleApply() + titleTextNode.frame = CGRect( + origin: CGPoint(x: titleMaxX - titleLayout.size.width, y: contentOriginY), + size: titleLayout.size + ) + } + if let (valueLayout, valueApply) = valueLayoutAndApply { + if valueTextNode.supernode == nil { + strongSelf.offsetContainer.addSubnode(valueTextNode) + } + let _ = valueApply() + valueTextNode.frame = CGRect( + origin: CGPoint(x: valueMinX, y: contentOriginY), + size: valueLayout.size + ) + contentOriginY += valueLayout.size.height + verticalSpacing + } + } + + positionAttributeNodes( + titleTextNode: strongSelf.registrationDateTitleTextNode, + valueTextNode: strongSelf.registrationDateValueTextNode, + titleLayoutAndApply: registrationDateTitleLayoutAndApply, + valueLayoutAndApply: registrationDateValueLayoutAndApply + ) + positionAttributeNodes( + titleTextNode: strongSelf.phoneCountryTitleTextNode, + valueTextNode: strongSelf.phoneCountryValueTextNode, + titleLayoutAndApply: phoneCountryTitleLayoutAndApply, + valueLayoutAndApply: phoneCountryValueLayoutAndApply + ) + positionAttributeNodes( + titleTextNode: strongSelf.locationCountryTitleTextNode, + valueTextNode: strongSelf.locationCountryValueTextNode, + titleLayoutAndApply: locationCountryTitleLayoutAndApply, + valueLayoutAndApply: locationCountryValueLayoutAndApply + ) + + contentOriginY += verticalSpacing + paragraphSpacing + let _ = groupsApply() + let groupsFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + floor((backgroundSize.width - groupsLayout.size.width) / 2.0), y: contentOriginY), size: groupsLayout.size) + strongSelf.groupsTextNode.frame = groupsFrame + + if strongSelf.backgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + backgroundContent.clipsToBounds = true + strongSelf.backgroundContent = backgroundContent + strongSelf.offsetContainer.insertSubnode(backgroundContent, at: 0) + } + + if let backgroundContent = strongSelf.backgroundContent { + backgroundContent.cornerRadius = item.presentationData.chatBubbleCorners.mainRadius + backgroundContent.frame = backgroundFrame + if let (rect, containerSize) = strongSelf.absolutePosition { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += containerSize.height - rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) + } + } + } + }) + } + } + + override public func updateTrailingItemSpace(_ height: CGFloat, transition: ContainedViewLayoutTransition) { + if height.isLessThanOrEqualTo(0.0) { + transition.updateFrame(node: self.offsetContainer, frame: CGRect(origin: CGPoint(), size: self.offsetContainer.bounds.size)) + } else { + transition.updateFrame(node: self.offsetContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: -floorToScreenPixels(height / 2.0)), size: self.offsetContainer.bounds.size)) + } + } + + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) + } + + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) + } + + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) + } + + override public func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + let result = super.point(inside: point, with: event) + let extra = self.offsetContainer.frame.contains(point) + return result || extra + } + +// public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { +// let textNodeFrame = self.textNode.frame +// if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - self.offsetContainer.frame.minX - textNodeFrame.minX, y: point.y - self.offsetContainer.frame.minY - textNodeFrame.minY)) { +// if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { +// var concealed = true +// if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { +// concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) +// } +// return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed))) +// } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { +// return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)) +// } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { +// return ChatMessageBubbleContentTapAction(content: .textMention(peerName)) +// } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { +// return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand)) +// } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { +// return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag)) +// } else { +// return ChatMessageBubbleContentTapAction(content: .none) +// } +// } else { +// return ChatMessageBubbleContentTapAction(content: .none) +// } +// } + +// @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { +// switch recognizer.state { +// case .ended: +// if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { +// switch gesture { +// case .tap: +// let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false) +// switch tapAction.content { +// case .none, .ignore: +// break +// case let .url(url): +// self.item?.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url.url, concealed: url.concealed, progress: tapAction.activate?())) +// case let .peerMention(peerId, _, _): +// if let item = self.item { +// let _ = (item.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) +// |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in +// if let peer = peer { +// self?.item?.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) +// } +// }) +// } +// case let .textMention(name): +// self.item?.controllerInteraction.openPeerMention(name, tapAction.activate?()) +// case let .botCommand(command): +// self.item?.controllerInteraction.sendBotCommand(nil, command) +// case let .hashtag(peerName, hashtag): +// self.item?.controllerInteraction.openHashtag(peerName, hashtag) +// default: +// break +// } +// case .longTap, .doubleTap: +// if let item = self.item, self.backgroundNode.frame.contains(location) { +// let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false) +// switch tapAction.content { +// case .none, .ignore: +// break +// case let .url(url): +// item.controllerInteraction.longTap(.url(url.url), ChatControllerInteraction.LongTapParams()) +// case let .peerMention(peerId, mention, _): +// item.controllerInteraction.longTap(.peerMention(peerId, mention), ChatControllerInteraction.LongTapParams()) +// case let .textMention(name): +// item.controllerInteraction.longTap(.mention(name), ChatControllerInteraction.LongTapParams()) +// case let .botCommand(command): +// item.controllerInteraction.longTap(.command(command), ChatControllerInteraction.LongTapParams()) +// case let .hashtag(_, hashtag): +// item.controllerInteraction.longTap(.hashtag(hashtag), ChatControllerInteraction.LongTapParams()) +// default: +// break +// } +// } +// default: +// break +// } +// } +// default: +// break +// } +// } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 903ed98646..7cbe0d82ae 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -93,6 +93,7 @@ public final class GiftItemComponent: Component { let context: AccountContext let theme: PresentationTheme + let strings: PresentationStrings let peer: GiftItemComponent.Peer? let subject: GiftItemComponent.Subject let title: String? @@ -108,6 +109,7 @@ public final class GiftItemComponent: Component { public init( context: AccountContext, theme: PresentationTheme, + strings: PresentationStrings, peer: GiftItemComponent.Peer? = nil, subject: GiftItemComponent.Subject, title: String? = nil, @@ -122,6 +124,7 @@ public final class GiftItemComponent: Component { ) { self.context = context self.theme = theme + self.strings = strings self.peer = peer self.subject = subject self.title = title @@ -142,6 +145,9 @@ public final class GiftItemComponent: Component { if lhs.theme !== rhs.theme { return false } + if lhs.strings !== rhs.strings { + return false + } if lhs.peer != rhs.peer { return false } @@ -471,8 +477,7 @@ public final class GiftItemComponent: Component { price = priceValue case .uniqueGift: buttonColor = UIColor.white - //TODO:localize - price = "Transfer" + price = component.strings.Gift_Options_Gift_Transfer } let buttonSize = self.button.update( @@ -501,10 +506,11 @@ public final class GiftItemComponent: Component { } if let label = component.label { + let labelColor = component.theme.overallDarkAppearance ? UIColor(rgb: 0xffc337) : UIColor(rgb: 0xd3720a) let attributes = MarkdownAttributes( - body: MarkdownAttributeSet(font: Font.regular(11.0), textColor: UIColor(rgb: 0xd3720a)), - bold: MarkdownAttributeSet(font: Font.semibold(11.0), textColor: UIColor(rgb: 0xd3720a)), - link: MarkdownAttributeSet(font: Font.regular(11.0), textColor: UIColor(rgb: 0xd3720a)), + body: MarkdownAttributeSet(font: Font.regular(11.0), textColor: labelColor), + bold: MarkdownAttributeSet(font: Font.semibold(11.0), textColor: labelColor), + link: MarkdownAttributeSet(font: Font.regular(11.0), textColor: labelColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index d64f3533ad..d45f5fc0b9 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -402,6 +402,7 @@ final class GiftOptionsScreenComponent: Component { GiftItemComponent( context: component.context, theme: environment.theme, + strings: environment.strings, peer: nil, subject: subject, ribbon: ribbon, @@ -501,9 +502,7 @@ final class GiftOptionsScreenComponent: Component { return } - //TODO:unmock let context = component.context - let alertController = giftTransferAlertController( context: context, gift: transferGift, @@ -516,36 +515,53 @@ final class GiftOptionsScreenComponent: Component { guard let controller, let navigationController = controller.navigationController as? NavigationController else { return } - var controllers = navigationController.viewControllers - controllers = controllers.filter { !($0 is ContactSelectionController) && !($0 is GiftOptionsScreen) } + 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 controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) } var foundController = false for controller in controllers.reversed() { - if let chatController = controller as? ChatController, case .peer(id: peer.id) = chatController.chatLocation { + 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 = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.default), params: nil) + 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) } + navigationController.setViewControllers(controllers, animated: true) + } + + if let completion = component.completion { + completion() } - navigationController.setViewControllers(controllers, animated: true) } ) controller.present(alertController, in: .window(.root)) @@ -713,14 +729,16 @@ final class GiftOptionsScreenComponent: Component { environment: {}, containerSize: availableSize ) + + let formattedBalance = formatStarsAmountText(self.starsState?.balance ?? StarsAmount.zero, dateTimeFormat: environment.dateTimeFormat) + let smallLabelFont = Font.regular(11.0) + let labelFont = Font.semibold(14.0) + let balanceText = tonAmountAttributedString(formattedBalance, integralFont: labelFont, fractionalFont: smallLabelFont, color: environment.theme.actionSheet.primaryTextColor, decimalSeparator: environment.dateTimeFormat.decimalSeparator) + let balanceValueSize = self.balanceValue.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: presentationStringsFormattedNumber(self.starsState?.balance ?? StarsAmount.zero, environment.dateTimeFormat.groupingSeparator), - font: Font.semibold(14.0), - textColor: environment.theme.actionSheet.primaryTextColor - )), + text: .plain(balanceText), maximumNumberOfLines: 1 )), environment: {}, @@ -855,12 +873,15 @@ final class GiftOptionsScreenComponent: Component { contentHeight += 6.0 } else { if let premiumProducts = state.premiumProducts { - //TODO:unmock - let premiumOptionSize = CGSize(width: optionWidth, height: 178.0 + 23.0) + var premiumOptionSize = CGSize(width: optionWidth, height: 178.0) var validIds: [AnyHashable] = [] var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: premiumOptionSize) for product in premiumProducts { + if let _ = product.starsPrice { + premiumOptionSize.height = 178.0 + 23.0 + } + let itemId = AnyHashable(product.id) validIds.append(itemId) @@ -885,8 +906,7 @@ final class GiftOptionsScreenComponent: Component { default: title = strings.Gift_Options_Premium_Months(3) } - - //TODO:unmock + let _ = visibleItem.update( transition: itemTransition, component: AnyComponent( @@ -895,11 +915,12 @@ final class GiftOptionsScreenComponent: Component { GiftItemComponent( context: component.context, theme: theme, + strings: environment.strings, peer: nil, subject: .premium(months: product.months, price: product.price), title: title, subtitle: strings.Gift_Options_Premium_Premium, - label: "or **#1500**", + label: product.starsPrice.flatMap { strings.Gift_Options_Premium_OrStars("**#\(presentationStringsFormattedNumber(Int32($0), environment.dateTimeFormat.groupingSeparator))**").string }, ribbon: product.discount.flatMap { GiftItemComponent.Ribbon( text: "-\($0)%", @@ -1047,10 +1068,9 @@ final class GiftOptionsScreenComponent: Component { )) if let transferStarGifts = self.state?.transferStarGifts, !transferStarGifts.isEmpty { - //TODO:localize tabSelectorItems.append(TabSelectorComponent.Item( id: AnyHashable(StarsFilter.transfer.rawValue), - title: "My Gifts" + title: strings.Gift_Options_Gift_Filter_MyGifts )) } @@ -1224,6 +1244,7 @@ final class GiftOptionsScreenComponent: Component { botUrl: "", storeProductId: option.storeProductId ), + starsGiftOption: nil, storeProduct: nil, discount: nil ) @@ -1243,7 +1264,14 @@ final class GiftOptionsScreenComponent: Component { if let product = availableProducts.first(where: { $0.id == option.storeProductId }), !product.isSubscription { let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(option.months) / Float(shortestOptionPrice.0) let discountValue = Int(round((1.0 - fraction) * 20.0) * 5.0) - premiumProducts.append(PremiumGiftProduct(giftOption: option, storeProduct: product, discount: discountValue > 0 ? discountValue : nil)) + let starsGiftOption = premiumOptions.first(where: { $0.currency == "XTR" && $0.months == option.months }) + + premiumProducts.append(PremiumGiftProduct( + giftOption: option, + starsGiftOption: starsGiftOption, + storeProduct: product, + discount: discountValue > 0 ? discountValue : nil + )) } } self.premiumProducts = premiumProducts.sorted(by: { $0.months < $1.months }) diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 12ed5cea42..8de9064abd 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -129,6 +129,7 @@ final class GiftSetupScreenComponent: Component { private var recenterOnTag: NSObject? private var peerMap: [EnginePeer.Id: EnginePeer] = [:] + private var sendPaidMessageStars: StarsAmount? private var starImage: (UIImage, PresentationTheme)? @@ -223,24 +224,29 @@ final class GiftSetupScreenComponent: Component { return } switch component.subject { - case .premium: - if self.payWithStars { + case let .premium(product): + if self.payWithStars, let starsPrice = product.starsPrice, let peer = self.peerMap[component.peerId] { //TODO:localize - let controller = textAlertController( - context: component.context, - title: "Send a Gift", - text: "Are you sure you want to gift **Telegram Premium** to Alicia for **1500 Stars**?", - actions: [ - TextAlertAction(type: .genericAction, title: "Cancel", action: {}), - TextAlertAction(type: .defaultAction, title: "Confirm", action: { [weak self] in - if let self { - self.proceedWithPremiumGift() - } - }) - ], - parseMarkdown: true - ) - environment.controller()?.present(controller, in: .window(.root)) + if let balance = component.context.starsContext?.currentState?.balance, balance.value < starsPrice { + self.proceedWithStarGift() + } else { + let priceString = presentationStringsFormattedNumber(Int32(starsPrice), environment.dateTimeFormat.groupingSeparator) + let controller = textAlertController( + context: component.context, + title: "Send a Gift", + text: "Are you sure you want to gift **Telegram Premium** to \(peer.compactDisplayTitle) for **\(priceString) Stars**?", + actions: [ + TextAlertAction(type: .genericAction, title: "Cancel", action: {}), + TextAlertAction(type: .defaultAction, title: "Confirm", action: { [weak self] in + if let self { + self.proceedWithStarGift() + } + }) + ], + parseMarkdown: true + ) + environment.controller()?.present(controller, in: .window(.root)) + } } else { self.proceedWithPremiumGift() } @@ -343,7 +349,7 @@ final class GiftSetupScreenComponent: Component { } private func proceedWithStarGift() { - guard let component = self.component, case let .starGift(starGift) = component.subject, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else { + guard let component = self.component, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else { return } @@ -351,9 +357,24 @@ final class GiftSetupScreenComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let peerId = component.peerId - var finalPrice = starGift.price - if self.includeUpgrade, let upgradeStars = starGift.upgradeStars { - finalPrice += upgradeStars + let entities = generateChatInputTextEntities(self.textInputState.text) + + var finalPrice: Int64 + let source: BotPaymentInvoiceSource + switch component.subject { + case let .premium(product): + if let option = product.starsGiftOption { + finalPrice = option.amount + source = .premiumGift(peerId: peerId, option: option, text: self.textInputState.text.string, entities: entities) + } else { + fatalError() + } + case let .starGift(starGift): + finalPrice = starGift.price + if self.includeUpgrade, let upgradeStars = starGift.upgradeStars { + finalPrice += upgradeStars + } + source = .starGift(hideName: self.hideName, includeUpgrade: self.includeUpgrade, peerId: peerId, giftId: starGift.id, text: self.textInputState.text.string, entities: entities) } let proceed = { [weak self] in @@ -364,10 +385,6 @@ final class GiftSetupScreenComponent: Component { self.inProgress = true self.state?.updated() - let entities = generateChatInputTextEntities(self.textInputState.text) - let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, includeUpgrade: self.includeUpgrade, peerId: peerId, giftId: starGift.id, text: self.textInputState.text.string, entities: entities) - - let completion = component.completion let signal = BotCheckoutController.InputData.fetch(context: component.context, source: source) @@ -384,7 +401,7 @@ final class GiftSetupScreenComponent: Component { return } - if peerId.namespace == Namespaces.Peer.CloudChannel { + if peerId.namespace == Namespaces.Peer.CloudChannel, case let .starGift(starGift) = component.subject { var controllers = navigationController.viewControllers controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) } navigationController.setViewControllers(controllers, animated: true) @@ -544,9 +561,10 @@ final class GiftSetupScreenComponent: Component { let _ = (component.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId), - TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId) + TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId), + TelegramEngine.EngineData.Item.Peer.SendPaidMessageStars(id: component.peerId) ) - |> deliverOnMainQueue).start(next: { [weak self] peer, accountPeer in + |> deliverOnMainQueue).start(next: { [weak self] peer, accountPeer, sendPaidMessageStars in guard let self else { return } @@ -556,6 +574,7 @@ final class GiftSetupScreenComponent: Component { if let accountPeer { self.peerMap[accountPeer.id] = accountPeer } + self.sendPaidMessageStars = sendPaidMessageStars self.state?.updated() }) @@ -661,15 +680,15 @@ final class GiftSetupScreenComponent: Component { } ) + self.optionsDisposable = (component.context.engine.payments.starsTopUpOptions() + |> deliverOnMainQueue).start(next: { [weak self] options in + guard let self else { + return + } + self.options = options + }) + if case let .starGift(gift) = component.subject { - self.optionsDisposable = (component.context.engine.payments.starsTopUpOptions() - |> deliverOnMainQueue).start(next: { [weak self] options in - guard let self else { - return - } - self.options = options - }) - if let _ = gift.upgradeStars { self.previewPromise.set( component.context.engine.payments.starGiftUpgradePreview(giftId: gift.id) @@ -732,9 +751,6 @@ final class GiftSetupScreenComponent: Component { let total: Int32 = availability.total let position = CGFloat(remains) / CGFloat(total) let sold = total - remains - //let remainsString = presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator) - //let soldString = presentationStringsFormattedNumber(total - remains, environment.dateTimeFormat.groupingSeparator) - //let totalString = presentationStringsFormattedNumber(total, environment.dateTimeFormat.groupingSeparator) let remainingCountSize = self.remainingCount.update( transition: transition, component: AnyComponent(RemainingCountComponent( @@ -772,50 +788,53 @@ final class GiftSetupScreenComponent: Component { var introSectionItems: [AnyComponentWithIdentity] = [] introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(Rectangle(color: .clear, height: 346.0, tag: self.introPlaceholderTag)))) - introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(ListMultilineTextFieldItemComponent( - externalState: self.textInputState, - context: component.context, - theme: environment.theme, - strings: environment.strings, - initialText: "", - resetText: self.resetText.flatMap { - return ListMultilineTextFieldItemComponent.ResetText(value: $0) - }, - placeholder: environment.strings.Gift_Send_Customize_MessagePlaceholder, - autocapitalizationType: .sentences, - autocorrectionType: .yes, - returnKeyType: .done, - characterLimit: Int(giftConfiguration.maxCaptionLength), - displayCharacterLimit: true, - emptyLineHandling: .notAllowed, - formatMenuAvailability: .available([.bold, .italic, .underline, .strikethrough, .spoiler]), - updated: { _ in - }, - returnKeyAction: { [weak self] in - guard let self else { - return - } - if let titleView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View { - titleView.endEditing(true) - } - }, - textUpdateTransition: .spring(duration: 0.4), - inputMode: self.currentInputMode, - toggleInputMode: { [weak self] in - guard let self else { - return - } - switch self.currentInputMode { - case .keyboard: - self.currentInputMode = .emoji - case .emoji: - self.currentInputMode = .keyboard - } - self.state?.updated(transition: .spring(duration: 0.4)) - }, - tag: self.textInputTag - )))) - self.resetText = nil + + if self.sendPaidMessageStars == nil { + introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(ListMultilineTextFieldItemComponent( + externalState: self.textInputState, + context: component.context, + theme: environment.theme, + strings: environment.strings, + initialText: "", + resetText: self.resetText.flatMap { + return ListMultilineTextFieldItemComponent.ResetText(value: $0) + }, + placeholder: environment.strings.Gift_Send_Customize_MessagePlaceholder, + autocapitalizationType: .sentences, + autocorrectionType: .yes, + returnKeyType: .done, + characterLimit: Int(giftConfiguration.maxCaptionLength), + displayCharacterLimit: true, + emptyLineHandling: .notAllowed, + formatMenuAvailability: .available([.bold, .italic, .underline, .strikethrough, .spoiler]), + updated: { _ in + }, + returnKeyAction: { [weak self] in + guard let self else { + return + } + if let titleView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View { + titleView.endEditing(true) + } + }, + textUpdateTransition: .spring(duration: 0.4), + inputMode: self.currentInputMode, + toggleInputMode: { [weak self] in + guard let self else { + return + } + switch self.currentInputMode { + case .keyboard: + self.currentInputMode = .emoji + case .emoji: + self.currentInputMode = .keyboard + } + self.state?.updated(transition: .spring(duration: 0.4)) + }, + tag: self.textInputTag + )))) + self.resetText = nil + } let footerAttributes = MarkdownAttributes( body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor), @@ -838,19 +857,7 @@ final class GiftSetupScreenComponent: Component { maximumNumberOfLines: 0 )) case .starGift: - //TODO:unmock - //TODO:localize - if self.textInputState.hasText { - introFooter = AnyComponent(MultilineTextComponent( - text: .markdown( - text: "**\(peerName)** charges **250 Stars** for each message. That price has been added to the cost of the gift.", - attributes: footerAttributes - ), - maximumNumberOfLines: 0 - )) - } else { - introFooter = nil - } + introFooter = nil } let introSectionSize = self.introSection.update( @@ -900,9 +907,8 @@ final class GiftSetupScreenComponent: Component { let subject: ChatGiftPreviewItem.Subject switch component.subject { case let .premium(product): - if self.payWithStars { - //TODO:unmock - subject = .premium(months: product.months, amount: 1500, currency: "XTR") + if self.payWithStars, let starsPrice = product.starsPrice { + subject = .premium(months: product.months, amount: starsPrice, currency: "XTR") } else { let (currency, amount) = product.storeProduct?.priceCurrencyAndAmount ?? ("USD", 1) subject = .premium(months: product.months, amount: amount, currency: currency) @@ -917,7 +923,6 @@ final class GiftSetupScreenComponent: Component { peers.append(peer) } - //TODO:unmock let introContentSize = self.introContent.update( transition: transition, component: AnyComponent( @@ -960,11 +965,12 @@ final class GiftSetupScreenComponent: Component { } switch component.subject { - case .premium: - //TODO:unmock - //TODO:localize - if "".isEmpty { - let starsFooterRawString = "Your balance is **# 147 988**. [Get More Stars >]()" + case let .premium(product): + if let starsPrice = product.starsPrice { + let balance = component.context.starsContext?.currentState?.balance.value ?? 0 + let balanceString = presentationStringsFormattedNumber(Int32(balance), environment.dateTimeFormat.groupingSeparator) + + let starsFooterRawString = environment.strings.Gift_Send_PayWithStars_Info("# \(balanceString)").string let starsFooterText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(starsFooterRawString, attributes: footerAttributes)) if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme { @@ -977,7 +983,8 @@ final class GiftSetupScreenComponent: Component { starsFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: starsFooterText.string)) } - let starsAttributedText = NSMutableAttributedString(string: "Pay with #1500", font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor) + let priceString = presentationStringsFormattedNumber(Int32(starsPrice), environment.dateTimeFormat.groupingSeparator) + let starsAttributedText = NSMutableAttributedString(string: environment.strings.Gift_Send_PayWithStars("#\(priceString)").string, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor) let range = (starsAttributedText.string as NSString).range(of: "#") if range.location != NSNotFound { starsAttributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range) @@ -997,6 +1004,7 @@ final class GiftSetupScreenComponent: Component { text: .plain(starsFooterText), maximumNumberOfLines: 0, highlightColor: environment.theme.list.itemAccentColor.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) @@ -1005,9 +1013,18 @@ final class GiftSetupScreenComponent: Component { } }, tapAction: { [weak self] _, _ in - guard let _ = self else { + guard let self, let component = self.component, let controller = self.environment?.controller(), let starsContext = component.context.starsContext else { return } + let _ = (self.optionsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { options in + let purchaseController = component.context.sharedContext.makeStarsPurchaseScreen(context: component.context, starsContext: starsContext, options: options ?? [], purpose: .generic, completion: { stars in + starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) + }) + controller.push(purchaseController) + }) } )), items: [ @@ -1243,10 +1260,9 @@ final class GiftSetupScreenComponent: Component { let buttonString: String switch component.subject { case let .premium(product): - if self.payWithStars { - //TODO:unmock - let amountString = presentationStringsFormattedNumber(Int32(1500), presentationData.dateTimeFormat.groupingSeparator) - buttonString = "\(environment.strings.Gift_Send_Send) # \(amountString)" + if self.payWithStars, let starsPrice = product.starsPrice { + let amountString = presentationStringsFormattedNumber(Int32(starsPrice), presentationData.dateTimeFormat.groupingSeparator) + buttonString = "\(environment.strings.Gift_Send_Send) # \(amountString)" } else { let amountString = product.price buttonString = "\(environment.strings.Gift_Send_Send) \(amountString)" @@ -1256,13 +1272,9 @@ final class GiftSetupScreenComponent: Component { if self.includeUpgrade, let upgradePrice = starGift.upgradeStars { finalPrice += upgradePrice } - //TODO:unmock - if self.textInputState.hasText { - finalPrice += 250 - } let amountString = presentationStringsFormattedNumber(Int32(finalPrice), presentationData.dateTimeFormat.groupingSeparator) let buttonTitle = isSelfGift ? environment.strings.Gift_Send_Buy : environment.strings.Gift_Send_Send - buttonString = "\(buttonTitle) # \(amountString)" + buttonString = "\(buttonTitle) # \(amountString)" if let availability = starGift.availability, availability.remains == 0 { buttonIsEnabled = false } @@ -1272,7 +1284,8 @@ final class GiftSetupScreenComponent: Component { if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.starImage?.0 { buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) buttonAttributedString.addAttribute(.foregroundColor, value: environment.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string)) - buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string)) + buttonAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: buttonAttributedString.string)) + buttonAttributedString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: buttonAttributedString.string)) } let buttonSize = self.button.update( @@ -1744,6 +1757,7 @@ private struct GiftConfiguration { public struct PremiumGiftProduct: Equatable { public let giftOption: CachedPremiumGiftOption + public let starsGiftOption: CachedPremiumGiftOption? public let storeProduct: InAppPurchaseManager.Product? public let discount: Int? @@ -1759,12 +1773,18 @@ public struct PremiumGiftProduct: Equatable { return self.storeProduct?.price ?? formatCurrencyAmount(self.giftOption.amount, currency: self.giftOption.currency) } - public var pricePerMonth: String { - return self.storeProduct?.pricePerMonth(Int(self.months)) ?? "" + public var starsPrice: Int64? { + return self.starsGiftOption?.amount } - public init(giftOption: CachedPremiumGiftOption, storeProduct: InAppPurchaseManager.Product?, discount: Int?) { + public init( + giftOption: CachedPremiumGiftOption, + starsGiftOption: CachedPremiumGiftOption?, + storeProduct: InAppPurchaseManager.Product?, + discount: Int? + ) { self.giftOption = giftOption + self.starsGiftOption = starsGiftOption self.storeProduct = storeProduct self.discount = discount } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift index 31105495aa..a9ff187f51 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift @@ -150,6 +150,7 @@ private final class GiftTransferAlertContentNode: AlertContentNode { GiftItemComponent( context: self.context, theme: self.presentationTheme, + strings: self.strings, peer: nil, subject: .uniqueGift(gift: self.gift), mode: .thumbnail diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 6ce5843024..4b7dff105c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -1745,7 +1745,7 @@ private final class GiftViewSheetContent: CombinedComponent { if let upgradeStars, upgradeStars > 0 { finalStars += upgradeStars } - let valueString = "⭐️\(presentationStringsFormattedNumber(abs(Int32(finalStars)), dateTimeFormat.groupingSeparator))" + let valueString = "\(presentationStringsFormattedNumber(abs(Int32(finalStars)), dateTimeFormat.groupingSeparator))⭐️" let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor) let range = (valueAttributedString.string as NSString).range(of: "⭐️") if range.location != NSNotFound { diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift index dbf9826806..6048815011 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift @@ -146,6 +146,7 @@ private final class GiftWithdrawAlertContentNode: AlertContentNode { GiftItemComponent( context: self.context, theme: self.presentationTheme, + strings: self.strings, peer: nil, subject: .uniqueGift(gift: self.gift), mode: .thumbnail diff --git a/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift b/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift index 7d8aa898ee..be2b344db3 100644 --- a/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift +++ b/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift @@ -253,7 +253,7 @@ public func presentedLegacyShortcutCamera(context: AccountContext, saveCapturedM nativeGenerator(_1, _2, _3, nil) }) if let parentController = parentController { - parentController.present(ShareController(context: context, subject: .fromExternal({ peerIds, _, text, account, silently in + parentController.present(ShareController(context: context, subject: .fromExternal(1, { peerIds, _, _, text, account, silently in guard let account = account as? ShareControllerAppAccountContext else { return .single(.done) } diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift index 79423fca6d..a0a6034849 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift @@ -214,6 +214,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { strings: presentationData.strings, style: .media, placeholder: .plain(presentationData.strings.MediaPicker_AddCaption), + sendPaidMessageStars: nil, maxLength: Int(self.context.userLimits.maxCaptionLength), queryTypes: [.mention, .hashtag], alwaysDarkWhenHasText: false, diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 15b3ca4613..86ed3d9a89 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1365,6 +1365,7 @@ final class MediaEditorScreenComponent: Component { strings: environment.strings, style: .editor, placeholder: .plain(environment.strings.Story_Editor_InputPlaceholderAddCaption), + sendPaidMessageStars: nil, maxLength: Int(component.context.userLimits.maxStoryCaptionLength), queryTypes: [.mention, .hashtag], alwaysDarkWhenHasText: false, diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift index 569e8f4273..900b4f8ba0 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift @@ -251,6 +251,7 @@ final class StoryPreviewComponent: Component { strings: presentationData.strings, style: .story, placeholder: .plain(presentationData.strings.Story_InputPlaceholderReplyPrivately), + sendPaidMessageStars: nil, maxLength: nil, queryTypes: [], alwaysDarkWhenHasText: false, diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 15ca03a305..aaf0c5e4a9 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -160,6 +160,7 @@ public final class MessageInputPanelComponent: Component { public let strings: PresentationStrings public let style: Style public let placeholder: Placeholder + public let sendPaidMessageStars: StarsAmount? public let maxLength: Int? public let queryTypes: ContextQueryTypes public let alwaysDarkWhenHasText: Bool @@ -218,6 +219,7 @@ public final class MessageInputPanelComponent: Component { strings: PresentationStrings, style: Style, placeholder: Placeholder, + sendPaidMessageStars: StarsAmount?, maxLength: Int?, queryTypes: ContextQueryTypes, alwaysDarkWhenHasText: Bool, @@ -276,6 +278,7 @@ public final class MessageInputPanelComponent: Component { self.style = style self.nextInputMode = nextInputMode self.placeholder = placeholder + self.sendPaidMessageStars = sendPaidMessageStars self.maxLength = maxLength self.queryTypes = queryTypes self.alwaysDarkWhenHasText = alwaysDarkWhenHasText @@ -346,6 +349,9 @@ public final class MessageInputPanelComponent: Component { if lhs.placeholder != rhs.placeholder { return false } + if lhs.sendPaidMessageStars != rhs.sendPaidMessageStars { + return false + } if lhs.maxLength != rhs.maxLength { return false } @@ -860,43 +866,75 @@ public final class MessageInputPanelComponent: Component { ) let isEditing = self.textFieldExternalState.isEditing || component.forceIsEditing - var placeholderItems: [AnimatedTextComponent.Item] = [] - switch component.placeholder { - case let .plain(string): - placeholderItems.append(AnimatedTextComponent.Item(id: AnyHashable(0 as Int), content: .text(string))) - case let .counter(items): - for item in items { - switch item.content { - case let .text(string): - placeholderItems.append(AnimatedTextComponent.Item(id: AnyHashable(item.id), content: .text(string))) - case let .number(value, minDigits): - placeholderItems.append(AnimatedTextComponent.Item(id: AnyHashable(item.id), content: .number(value, minDigits: minDigits))) + let placeholderTransition: ComponentTransition = (previousPlaceholder != nil && previousPlaceholder != component.placeholder) ? ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) : .immediate + let placeholderSize: CGSize + if case let .plain(string) = component.placeholder, string.contains("#") { + let attributedPlaceholder = NSMutableAttributedString(string: string, font:Font.regular(17.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.3)) + if let range = attributedPlaceholder.string.range(of: "#") { + attributedPlaceholder.addAttribute(.attachment, value: PresentationResourcesChat.chatPlaceholderStarIcon(component.theme)!, range: NSRange(range, in: attributedPlaceholder.string)) + attributedPlaceholder.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff, alpha: 0.3), range: NSRange(range, in: attributedPlaceholder.string)) + attributedPlaceholder.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedPlaceholder.string)) + } + + placeholderSize = self.placeholder.update( + transition: placeholderTransition, + component: AnyComponent(MultilineTextComponent(text: .plain(attributedPlaceholder))), + environment: {}, + containerSize: availableTextFieldSize + ) + + let vibrancyAttributedPlaceholder = NSMutableAttributedString(string: string, font:Font.regular(17.0), textColor: UIColor.black) + if let range = vibrancyAttributedPlaceholder.string.range(of: "#") { + vibrancyAttributedPlaceholder.addAttribute(.attachment, value: PresentationResourcesChat.chatPlaceholderStarIcon(component.theme)!, range: NSRange(range, in: vibrancyAttributedPlaceholder.string)) + vibrancyAttributedPlaceholder.addAttribute(.foregroundColor, value: UIColor.black, range: NSRange(range, in: vibrancyAttributedPlaceholder.string)) + vibrancyAttributedPlaceholder.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: vibrancyAttributedPlaceholder.string)) + } + + let _ = self.vibrancyPlaceholder.update( + transition: placeholderTransition, + component: AnyComponent(MultilineTextComponent(text: .plain(attributedPlaceholder))), + environment: {}, + containerSize: availableTextFieldSize + ) + } else { + var placeholderItems: [AnimatedTextComponent.Item] = [] + switch component.placeholder { + case let .plain(string): + placeholderItems.append(AnimatedTextComponent.Item(id: AnyHashable(0 as Int), content: .text(string))) + case let .counter(items): + for item in items { + switch item.content { + case let .text(string): + placeholderItems.append(AnimatedTextComponent.Item(id: AnyHashable(item.id), content: .text(string))) + case let .number(value, minDigits): + placeholderItems.append(AnimatedTextComponent.Item(id: AnyHashable(item.id), content: .number(value, minDigits: minDigits))) + } } } + + placeholderSize = self.placeholder.update( + transition: placeholderTransition, + component: AnyComponent(AnimatedTextComponent( + font: Font.regular(17.0), + color: UIColor(rgb: 0xffffff, alpha: 0.3), + items: placeholderItems + )), + environment: {}, + containerSize: availableTextFieldSize + ) + + let _ = self.vibrancyPlaceholder.update( + transition: placeholderTransition, + component: AnyComponent(AnimatedTextComponent( + font: Font.regular(17.0), + color: .black, + items: placeholderItems + )), + environment: {}, + containerSize: availableTextFieldSize + ) } - let placeholderTransition: ComponentTransition = (previousPlaceholder != nil && previousPlaceholder != component.placeholder) ? ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) : .immediate - let placeholderSize = self.placeholder.update( - transition: placeholderTransition, - component: AnyComponent(AnimatedTextComponent( - font: Font.regular(17.0), - color: UIColor(rgb: 0xffffff, alpha: 0.4), - items: placeholderItems - )), - environment: {}, - containerSize: availableTextFieldSize - ) - - let _ = self.vibrancyPlaceholder.update( - transition: placeholderTransition, - component: AnyComponent(AnimatedTextComponent( - font: Font.regular(17.0), - color: .black, - items: placeholderItems - )), - environment: {}, - containerSize: availableTextFieldSize - ) if !isEditing && component.setMediaRecordingActive == nil { insets.right = defaultInsets.left } diff --git a/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/BUILD b/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/BUILD index 540a813d62..adbe07190b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/BUILD @@ -19,6 +19,10 @@ swift_library( "//submodules/TelegramPresentationData", "//submodules/ItemListUI", "//submodules/LegacyComponents", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/MultilineTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/Sources/MessagePriceItem.swift b/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/Sources/MessagePriceItem.swift index 2e4e2978e7..598a80f7e0 100644 --- a/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/Sources/MessagePriceItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/Sources/MessagePriceItem.swift @@ -8,6 +8,10 @@ import TelegramPresentationData import LegacyComponents import ItemListUI import PresentationDataUtils +import ComponentFlow +import ButtonComponent +import BundleIconComponent +import MultilineTextComponent private let textFont = Font.with(size: 17.0, traits: .monospacedNumbers) private let smallTextFont = Font.with(size: 13.0, traits: .monospacedNumbers) @@ -15,22 +19,26 @@ private let smallTextFont = Font.with(size: 13.0, traits: .monospacedNumbers) public final class MessagePriceItem: ListViewItem, ItemListItem { let theme: PresentationTheme let strings: PresentationStrings + let isEnabled: Bool let minValue: Int64 let maxValue: Int64 let value: Int64 let price: String public let sectionId: ItemListSectionId let updated: (Int64) -> Void + let openPremiumInfo: (() -> Void)? - public init(theme: PresentationTheme, strings: PresentationStrings, minValue: Int64, maxValue: Int64, value: Int64, price: String, sectionId: ItemListSectionId, updated: @escaping (Int64) -> 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) { self.theme = theme self.strings = strings + self.isEnabled = isEnabled self.minValue = minValue self.maxValue = maxValue self.value = value self.price = price self.sectionId = sectionId self.updated = updated + self.openPremiumInfo = openPremiumInfo } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -155,6 +163,9 @@ private class MessagePriceItemNode: ListViewItemNode { private let rightTextNode: ImmediateTextNode private let centerLeftTextNode: ImmediateTextNode private let centerRightTextNode: ImmediateTextNode + private let lockIconNode: ASImageNode + + private let button: ComponentView private var amount: Amount = Amount(realValue: 1, maxRealValue: 1000, maxSliderValue: 1000, isLogarithmic: true) @@ -178,12 +189,18 @@ private class MessagePriceItemNode: ListViewItemNode { self.centerLeftTextNode = ImmediateTextNode() self.centerRightTextNode = ImmediateTextNode() + self.lockIconNode = ASImageNode() + self.lockIconNode.displaysAsynchronously = false + + self.button = ComponentView() + super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.leftTextNode) self.addSubnode(self.rightTextNode) self.addSubnode(self.centerLeftTextNode) self.addSubnode(self.centerRightTextNode) + self.addSubnode(self.lockIconNode) } override func didLoad() { @@ -224,11 +241,15 @@ private class MessagePriceItemNode: ListViewItemNode { themeUpdated = true } - let contentSize: CGSize + var contentSize: CGSize let insets: UIEdgeInsets let separatorHeight = UIScreenPixel contentSize = CGSize(width: params.width, height: 88.0) + if !item.isEnabled { + contentSize.height = 166.0 + } + insets = itemListNeighborsGroupedInsets(neighbors, params) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -290,8 +311,7 @@ private class MessagePriceItemNode: ListViewItemNode { strongSelf.leftTextNode.attributedText = NSAttributedString(string: "\(item.minValue)", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor) strongSelf.rightTextNode.attributedText = NSAttributedString(string: "\(item.maxValue)", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor) - //TODO:localize - let centralLeftText = "\(item.value) Stars" + let centralLeftText = item.strings.Privacy_Messages_Stars(Int32(item.value)) strongSelf.centerLeftTextNode.attributedText = NSAttributedString(string: centralLeftText, font: textFont, textColor: item.theme.list.itemPrimaryTextColor) strongSelf.centerRightTextNode.attributedText = NSAttributedString(string: item.price, font: smallTextFont, textColor: item.theme.list.itemSecondaryTextColor) @@ -323,6 +343,66 @@ 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)) } + + strongSelf.lockIconNode.isHidden = item.isEnabled + if !item.isEnabled { + if themeUpdated { + strongSelf.lockIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/SmallLock"), color: item.theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.5)) + } + if let image = strongSelf.lockIconNode.image { + strongSelf.lockIconNode.frame = CGRect(origin: CGPoint(x: centerLeftFrame.minX - image.size.width - 1.0, y: 12.0 + UIScreenPixel), size: image.size) + } + + let sideInset: CGFloat = 16.0 + let buttonSize = CGSize(width: params.width - params.leftInset - params.rightInset - sideInset * 2.0, height: 50.0) + let _ = strongSelf.button.update( + transition: .immediate, + component: AnyComponent( + ButtonComponent( + background: ButtonComponent.Background( + color: item.theme.list.itemCheckColors.fillColor, + foreground: item.theme.list.itemCheckColors.foregroundColor, + pressedColor: item.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable("unlock"), + component: AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: AnyHashable("icon"), + component: AnyComponent(BundleIconComponent(name: "Chat/Stickers/Lock", tintColor: item.theme.list.itemCheckColors.foregroundColor)) + ), + AnyComponentWithIdentity( + id: AnyHashable("label"), + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: item.strings.Privacy_Messages_Unlock, font: Font.semibold(17.0), textColor: item.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) + ) + ], spacing: 3.0) + ) + ), + isEnabled: true, + tintWhenDisabled: false, + allowActionWhenDisabled: false, + displaysProgress: false, + action: { [weak self] in + guard let self, let item = self.item else { + return + } + item.openPremiumInfo?() + } + ) + ), + environment: {}, + containerSize: buttonSize + ) + if let buttonView = strongSelf.button.view { + if buttonView.superview == nil { + strongSelf.view.addSubview(buttonView) + } + buttonView.frame = CGRect(origin: CGPoint(x: params.leftInset + sideInset, y: contentSize.height - buttonSize.height - sideInset), size: buttonSize) + } + } else if let buttonView = strongSelf.button.view, buttonView.superview != nil { + buttonView.removeFromSuperview() + } } }) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 9fcc7aad7c..5323b00e3e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1000,14 +1000,18 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p })) } if let starsState = data.starsState { - let balanceText: String - if starsState.balance > StarsAmount.zero { - balanceText = presentationStringsFormattedNumber(starsState.balance, presentationData.dateTimeFormat.groupingSeparator) - } else { - balanceText = "" - } if !isPremiumDisabled || starsState.balance > StarsAmount.zero { - items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 102, label: .text(balanceText), text: presentationData.strings.Settings_Stars, icon: PresentationResourcesSettings.stars, action: { + let balanceText: NSAttributedString + if starsState.balance > StarsAmount.zero { + let formattedLabel = formatStarsAmountText(starsState.balance, dateTimeFormat: presentationData.dateTimeFormat) + let smallLabelFont = Font.regular(floor(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 13.0)) + let labelFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize) + let labelColor = presentationData.theme.list.itemSecondaryTextColor + balanceText = tonAmountAttributedString(formattedLabel, integralFont: labelFont, fractionalFont: smallLabelFont, color: labelColor, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) + } else { + balanceText = NSAttributedString() + } + items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 102, label: .attributedText(balanceText), text: presentationData.strings.Settings_Stars, icon: PresentationResourcesSettings.stars, action: { interaction.openSettings(.stars) })) } @@ -1574,8 +1578,13 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } if overallStarsBalance > StarsAmount.zero { - let string = "*\(presentationStringsFormattedNumber(starsBalance, presentationData.dateTimeFormat.groupingSeparator))" - let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor) + let formattedLabel = formatStarsAmountText(starsBalance, dateTimeFormat: presentationData.dateTimeFormat) + let smallLabelFont = Font.regular(floor(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 13.0)) + let labelFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize) + let labelColor = presentationData.theme.list.itemSecondaryTextColor + let attributedString = tonAmountAttributedString(formattedLabel, integralFont: labelFont, fractionalFont: smallLabelFont, color: labelColor, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator).mutableCopy() as! NSMutableAttributedString + attributedString.insert(NSAttributedString(string: "*", font: labelFont, textColor: labelColor), at: 0) + if let range = attributedString.string.range(of: "*") { attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedString.string)) attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string)) @@ -1854,17 +1863,24 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese let overallStarsBalance = data.starsRevenueStatsState?.balances.overallRevenue ?? StarsAmount.zero if overallRevenueBalance > 0 || overallStarsBalance > StarsAmount.zero { - var string = "" + let smallLabelFont = Font.regular(floor(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 13.0)) + let labelFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize) + let labelColor = presentationData.theme.list.itemSecondaryTextColor + + let attributedString = NSMutableAttributedString() if overallRevenueBalance > 0 { - string.append("#\(formatTonAmountText(revenueBalance, dateTimeFormat: presentationData.dateTimeFormat))") + attributedString.append(NSAttributedString(string: "#\(formatTonAmountText(revenueBalance, dateTimeFormat: presentationData.dateTimeFormat))", font: labelFont, textColor: labelColor)) } if overallStarsBalance > StarsAmount.zero { - if !string.isEmpty { - string.append(" ") + if !attributedString.string.isEmpty { + attributedString.append(NSAttributedString(string: " ", font: labelFont, textColor: labelColor)) } - string.append("*\(presentationStringsFormattedNumber(starsBalance, presentationData.dateTimeFormat.groupingSeparator))") + attributedString.append(NSAttributedString(string: "*", font: labelFont, textColor: labelColor)) + + let formattedLabel = formatStarsAmountText(starsBalance, dateTimeFormat: presentationData.dateTimeFormat) + let starsAttributedString = tonAmountAttributedString(formattedLabel, integralFont: labelFont, fractionalFont: smallLabelFont, color: labelColor, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator).mutableCopy() as! NSMutableAttributedString + attributedString.append(starsAttributedString) } - let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor) if let range = attributedString.string.range(of: "#") { attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton), range: NSRange(range, in: attributedString.string)) attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string)) @@ -2048,9 +2064,14 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL })) } else if !user.flags.contains(.isSupport) { let compactName = EnginePeer(user).compactDisplayTitle - items[.peerDataSettings]!.append(PeerInfoScreenActionItem(id: ItemSuggest, text: presentationData.strings.UserInfo_SuggestPhoto(compactName).string, color: .accent, icon: UIImage(bundleImageName: "Peer Info/SuggestAvatar"), action: { - interaction.suggestPhoto() - })) + + if let cachedData = data.cachedData as? CachedUserData, let _ = cachedData.sendPaidMessageStars { + + } else { + items[.peerDataSettings]!.append(PeerInfoScreenActionItem(id: ItemSuggest, text: presentationData.strings.UserInfo_SuggestPhoto(compactName).string, color: .accent, icon: UIImage(bundleImageName: "Peer Info/SuggestAvatar"), action: { + interaction.suggestPhoto() + })) + } let setText: String if user.photo.first?.isPersonal == true || state.updatingAvatar != nil { @@ -9824,7 +9845,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private func openPremiumGift() { let premiumGiftOptions = self.data?.premiumGiftOptions ?? [] let premiumOptions = premiumGiftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } - + var hasBirthday = false if let cachedUserData = self.data?.cachedData as? CachedUserData { hasBirthday = hasBirthdayToday(cachedData: cachedUserData) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 60232b1e43..1dc7a26e96 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -231,6 +231,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr GiftItemComponent( context: self.context, theme: params.presentationData.theme, + strings: params.presentationData.strings, peer: peer, subject: subject, ribbon: ribbonText.flatMap { GiftItemComponent.Ribbon(text: $0, color: ribbonColor) }, diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 5c2d28be55..45b32e5ec6 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -718,7 +718,8 @@ final class PeerSelectionControllerNode: ASDisplayNode { forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], canMakePaidContent: false, currentPrice: nil, - hasTimers: false + hasTimers: false, + sendPaidMessageStars: nil )), hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, diff --git a/submodules/TelegramUI/Components/SendInviteLinkScreen/BUILD b/submodules/TelegramUI/Components/SendInviteLinkScreen/BUILD index 0c0e555fad..e95817f52a 100644 --- a/submodules/TelegramUI/Components/SendInviteLinkScreen/BUILD +++ b/submodules/TelegramUI/Components/SendInviteLinkScreen/BUILD @@ -31,6 +31,7 @@ swift_library( "//submodules/PeerPresenceStatusManager", "//submodules/UndoUI", "//submodules/AnimatedAvatarSetNode", + "//submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift b/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift index 158d1accb0..3dfb23c301 100644 --- a/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift +++ b/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift @@ -17,6 +17,7 @@ import UndoUI import AnimatedAvatarSetNode import AvatarNode import TelegramStringFormatting +import ChatMessagePaymentAlertController private final class SendInviteLinkScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -26,19 +27,22 @@ private final class SendInviteLinkScreenComponent: Component { let link: String? let peers: [TelegramForbiddenInvitePeer] let peerPresences: [EnginePeer.Id: EnginePeer.Presence] + let sendPaidMessageStars: [EnginePeer.Id: StarsAmount] init( context: AccountContext, peer: EnginePeer, link: String?, peers: [TelegramForbiddenInvitePeer], - peerPresences: [EnginePeer.Id: EnginePeer.Presence] + peerPresences: [EnginePeer.Id: EnginePeer.Presence], + sendPaidMessageStars: [EnginePeer.Id: StarsAmount] ) { self.context = context self.peer = peer self.link = link self.peers = peers self.peerPresences = peerPresences + self.sendPaidMessageStars = sendPaidMessageStars } static func ==(lhs: SendInviteLinkScreenComponent, rhs: SendInviteLinkScreenComponent) -> Bool { @@ -54,6 +58,9 @@ private final class SendInviteLinkScreenComponent: Component { if lhs.peerPresences != rhs.peerPresences { return false } + if lhs.sendPaidMessageStars != rhs.sendPaidMessageStars { + return false + } return true } @@ -266,6 +273,38 @@ private final class SendInviteLinkScreenComponent: Component { } } + private func presentPaidMessageAlertIfNeeded(peers: [EnginePeer], requiresStars: [EnginePeer.Id: StarsAmount], completion: @escaping () -> Void) { + guard let component = self.component else { + completion() + return + } + var totalAmount: StarsAmount = .zero + for peer in peers { + if let amount = requiresStars[peer.id] { + totalAmount = totalAmount + amount + } + } + if totalAmount.value > 0 { + let controller = chatMessagePaymentAlertController( + context: component.context, + presentationData: component.context.sharedContext.currentPresentationData.with { $0 }, + updatedPresentationData: nil, + peers: peers, + count: 1, + amount: totalAmount, + totalAmount: totalAmount, + hasCheck: false, + navigationController: self.environment?.controller()?.navigationController as? NavigationController, + completion: { _ in + completion() + } + ) + self.environment?.controller()?.present(controller, in: .window(.root)) + } else { + completion() + } + } + func update(component: SendInviteLinkScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[ViewControllerComponentContainer.Environment.self].value let themeUpdated = self.environment?.theme !== environment.theme @@ -851,20 +890,37 @@ private final class SendInviteLinkScreenComponent: Component { } else if let link = component.link { let selectedPeers = component.peers.filter { self.selectedItems.contains($0.peer.id) } - let _ = enqueueMessagesToMultiplePeers(account: component.context.account, peerIds: Array(self.selectedItems), threadIds: [:], messages: [.message(text: link, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() - let text: String - if selectedPeers.count == 1 { - text = environment.strings.Conversation_ShareLinkTooltip_Chat_One(selectedPeers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string - } else if selectedPeers.count == 2 { - text = environment.strings.Conversation_ShareLinkTooltip_TwoChats_One(selectedPeers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), selectedPeers[1].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string - } else { - text = environment.strings.Conversation_ShareLinkTooltip_ManyChats_One(selectedPeers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), "\(selectedPeers.count - 1)").string - } - - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - controller.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: false, text: text), elevatedLayout: false, action: { _ in return false }), in: .window(.root)) - - controller.dismiss() + self.presentPaidMessageAlertIfNeeded( + peers: selectedPeers.map { $0.peer }, + requiresStars: component.sendPaidMessageStars, + completion: { [weak self] in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + + for peerId in Array(self.selectedItems) { + var messageAttributes: [EngineMessage.Attribute] = [] + if let sendPaidMessageStars = component.sendPaidMessageStars[peerId] { + messageAttributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: false)) + } + let _ = enqueueMessages(account: component.context.account, peerId: peerId, messages: [.message(text: link, attributes: messageAttributes, inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).startStandalone() + } + + let text: String + if selectedPeers.count == 1 { + text = environment.strings.Conversation_ShareLinkTooltip_Chat_One(selectedPeers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string + } else if selectedPeers.count == 2 { + text = environment.strings.Conversation_ShareLinkTooltip_TwoChats_One(selectedPeers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), selectedPeers[1].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string + } else { + text = environment.strings.Conversation_ShareLinkTooltip_ManyChats_One(selectedPeers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), "\(selectedPeers.count - 1)").string + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: false, text: text), elevatedLayout: false, action: { _ in return false }), in: .window(.root)) + + controller.dismiss() + } + ) } else { controller.dismiss() } @@ -1083,16 +1139,21 @@ public class SendInviteLinkScreen: ViewControllerComponentContainer { self.link = link self.peers = peers - super.init(context: context, component: SendInviteLinkScreenComponent(context: context, peer: peer, link: link, peers: peers, peerPresences: [:]), navigationBarAppearance: .none) + super.init(context: context, component: SendInviteLinkScreenComponent(context: context, peer: peer, link: link, peers: peers, peerPresences: [:], sendPaidMessageStars: [:]), navigationBarAppearance: .none) self.statusBar.statusBarStyle = .Ignore self.navigationPresentation = .flatModal self.blocksBackgroundWhenInOverlay = true - self.presenceDisposable = (context.engine.data.subscribe(EngineDataMap( - peers.map(\.peer.id).map(TelegramEngine.EngineData.Item.Peer.Presence.init(id:)) - )) - |> deliverOnMainQueue).start(next: { [weak self] presences in + self.presenceDisposable = (context.engine.data.subscribe( + EngineDataMap( + peers.map(\.peer.id).map(TelegramEngine.EngineData.Item.Peer.Presence.init(id:)) + ), + EngineDataMap( + peers.map(\.peer.id).map(TelegramEngine.EngineData.Item.Peer.SendPaidMessageStars.init(id:)) + ) + ) + |> deliverOnMainQueue).start(next: { [weak self] presences, sendPaidMessageStars in guard let self else { return } @@ -1102,7 +1163,13 @@ public class SendInviteLinkScreen: ViewControllerComponentContainer { parsedPresences[id] = presence } } - self.updateComponent(component: AnyComponent(SendInviteLinkScreenComponent(context: context, peer: peer, link: link, peers: peers, peerPresences: parsedPresences)), transition: .immediate) + var parsedSendPaidMessageStars: [EnginePeer.Id: StarsAmount] = [:] + for (id, sendPaidMessageStars) in sendPaidMessageStars { + if let sendPaidMessageStars { + parsedSendPaidMessageStars[id] = sendPaidMessageStars + } + } + self.updateComponent(component: AnyComponent(SendInviteLinkScreenComponent(context: context, peer: peer, link: link, peers: peers, peerPresences: parsedPresences, sendPaidMessageStars: parsedSendPaidMessageStars)), transition: .immediate) }) } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift index caf03a252b..3739f54bc2 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift @@ -123,6 +123,7 @@ final class GiftListItemComponent: Component { GiftItemComponent( context: component.context, theme: component.theme, + strings: component.context.sharedContext.currentPresentationData.with { $0 }.strings, peer: nil, subject: .uniqueGift(gift: gift), ribbon: nil, diff --git a/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift b/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift index f33b368637..a02526928e 100644 --- a/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift +++ b/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift @@ -525,8 +525,8 @@ public class ShareRootControllerImpl { } |> runOn(Queue.mainQueue()) } - let sentItems: ([PeerId], [PeerId: Int64], [PreparedShareItemContent], ShareControllerAccountContext, Bool, String) -> Signal = { peerIds, threadIds, contents, account, silently, additionalText in - let sentItems = sentShareItems(accountPeerId: account.accountPeerId, postbox: account.stateManager.postbox, network: account.stateManager.network, stateManager: account.stateManager, auxiliaryMethods: makeTelegramAccountAuxiliaryMethods(uploadInBackground: nil), to: peerIds, threadIds: threadIds, items: contents, silently: silently, additionalText: additionalText) + let sentItems: ([PeerId], [PeerId: Int64], [PeerId: StarsAmount], [PreparedShareItemContent], ShareControllerAccountContext, Bool, String) -> Signal = { peerIds, threadIds, requireStars, contents, account, silently, additionalText in + let sentItems = sentShareItems(accountPeerId: account.accountPeerId, postbox: account.stateManager.postbox, network: account.stateManager.network, stateManager: account.stateManager, auxiliaryMethods: makeTelegramAccountAuxiliaryMethods(uploadInBackground: nil), to: peerIds, threadIds: threadIds, requireStars: requireStars, items: contents, silently: silently, additionalText: additionalText) |> `catch` { _ -> Signal< Float, NoError> in return .complete() @@ -537,8 +537,20 @@ public class ShareRootControllerImpl { } |> then(.single(.done)) } - - let shareController = ShareController(environment: environment, currentContext: context, subject: .fromExternal({ peerIds, threadIds, additionalText, account, silently in + + var itemCount = 1 + + if let extensionItems = self?.getExtensionContext()?.inputItems as? [NSExtensionItem] { + for item in extensionItems { + if let attachments = item.attachments { + itemCount = 0 + for _ in attachments { + itemCount += 1 + } + } + } + } + let shareController = ShareController(environment: environment, currentContext: context, subject: .fromExternal(itemCount, { peerIds, threadIds, requireStars, additionalText, account, silently in if let strongSelf = self, let inputItems = strongSelf.getExtensionContext()?.inputItems, !inputItems.isEmpty, !peerIds.isEmpty { let rawSignals = TGItemProviderSignals.itemSignals(forInputItems: inputItems)! return preparedShareItems(postbox: account.stateManager.postbox, network: account.stateManager.network, to: peerIds[0], dataItems: rawSignals) @@ -564,11 +576,11 @@ public class ShareRootControllerImpl { return requestUserInteraction(value) |> castError(ShareControllerError.self) |> mapToSignal { contents -> Signal in - return sentItems(peerIds, threadIds, contents, account, silently, additionalText) + return sentItems(peerIds, threadIds, requireStars, contents, account, silently, additionalText) |> castError(ShareControllerError.self) } case let .done(contents): - return sentItems(peerIds, threadIds, contents, account, silently, additionalText) + return sentItems(peerIds, threadIds, requireStars, contents, account, silently, additionalText) |> castError(ShareControllerError.self) } } diff --git a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift index 6b3a895a7d..b7abee888a 100644 --- a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift @@ -124,6 +124,7 @@ public final class StarsAvatarComponent: Component { GiftItemComponent( context: component.context, theme: component.theme, + strings: component.context.sharedContext.currentPresentationData.with { $0 }.strings, peer: nil, subject: .uniqueGift(gift: gift), mode: .thumbnail diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index c31e71e94e..cf2e01e40e 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -241,6 +241,12 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { textString = strings.Stars_Purchase_StarGiftInfo(component.peers.first?.value.compactDisplayTitle ?? "").string case .upgradeStarGift: textString = strings.Stars_Purchase_UpgradeStarGiftInfo + case let .sendMessage(peerId, _): + if peerId.namespace == Namespaces.Peer.CloudUser { + textString = strings.Stars_Purchase_SendMessageInfo(component.peers.first?.value.compactDisplayTitle ?? "").string + } else { + textString = strings.Stars_Purchase_SendGroupMessageInfo(component.peers.first?.value.compactDisplayTitle ?? "").string + } } let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { contents in @@ -822,7 +828,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): + 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): titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars)) } @@ -849,14 +855,14 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { availableSize: context.availableSize, transition: .immediate ) - let starsBalance: StarsAmount = state.starsState?.balance ?? StarsAmount.zero + + let formattedBalance = formatStarsAmountText(state.starsState?.balance ?? StarsAmount.zero, dateTimeFormat: environment.dateTimeFormat) + let smallLabelFont = Font.regular(11.0) + let labelFont = Font.semibold(14.0) + let balanceText = tonAmountAttributedString(formattedBalance, integralFont: labelFont, fractionalFont: smallLabelFont, color: environment.theme.actionSheet.primaryTextColor, decimalSeparator: environment.dateTimeFormat.decimalSeparator) let balanceValue = balanceValue.update( component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: presentationStringsFormattedNumber(starsBalance, environment.dateTimeFormat.groupingSeparator), - font: Font.semibold(14.0), - textColor: environment.theme.actionSheet.primaryTextColor - )), + text: .plain(balanceText), maximumNumberOfLines: 1 ), availableSize: context.availableSize, @@ -1245,6 +1251,8 @@ private extension StarsPurchasePurpose { return [peerId] case let .starGift(peerId, _): return [peerId] + case let .sendMessage(peerId, _): + return [peerId] default: return [] } @@ -1266,6 +1274,8 @@ private extension StarsPurchasePurpose { return requiredStars case let .upgradeStarGift(requiredStars): return requiredStars + case let .sendMessage(_, requiredStars): + return requiredStars default: return nil } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD index ff340fd489..b4546dbe12 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD @@ -19,6 +19,7 @@ swift_library( "//submodules/Components/ViewControllerComponent", "//submodules/Components/ComponentDisplayAdapters", "//submodules/Components/MultilineTextComponent", + "//submodules/Components/MultilineTextWithEntitiesComponent", "//submodules/Components/BalancedTextComponent", "//submodules/TelegramPresentationData", "//submodules/AccountContext", diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index de35cf8bc7..c11468435f 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -12,6 +12,7 @@ import ComponentFlow import ViewControllerComponent import SheetComponent import MultilineTextComponent +import MultilineTextWithEntitiesComponent import BundleIconComponent import SolidRoundedButtonComponent import Markdown @@ -37,6 +38,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { let openMessage: (EngineMessage.Id) -> Void let openMedia: ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void let openAppExamples: () -> Void + let openPaidMessageFee: () -> Void let copyTransactionId: (String) -> Void let updateSubscription: () -> Void let sendGift: (EnginePeer.Id) -> Void @@ -49,6 +51,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { openMessage: @escaping (EngineMessage.Id) -> Void, openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void, openAppExamples: @escaping () -> Void, + openPaidMessageFee: @escaping () -> Void, copyTransactionId: @escaping (String) -> Void, updateSubscription: @escaping () -> Void, sendGift: @escaping (EnginePeer.Id) -> Void @@ -60,6 +63,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { self.openMessage = openMessage self.openMedia = openMedia self.openAppExamples = openAppExamples + self.openPaidMessageFee = openPaidMessageFee self.copyTransactionId = copyTransactionId self.updateSubscription = updateSubscription self.sendGift = sendGift @@ -238,6 +242,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { var isGiftUpgrade = false var giftAvailability: StarGift.Gift.Availability? var isRefProgram = false + var isPaidMessage = false var delayedCloseOnOpenPeer = true switch subject { @@ -414,14 +419,21 @@ private final class StarsTransactionSheetContent: CombinedComponent { isGift = true } else if let starrefCommissionPermille = transaction.starrefCommissionPermille { isRefProgram = true - if transaction.starrefPeerId == nil { + if transaction.flags.contains(.isPaidMessage) { + isPaidMessage = true + titleText = strings.Stars_Transaction_PaidMessage(transaction.paidMessageCount ?? 1) + countOnTop = true + descriptionText = strings.Stars_Transaction_PaidMessage_Text(formatPermille(starrefCommissionPermille)).string + } else if transaction.starrefPeerId == nil { titleText = strings.StarsTransaction_TitleCommission(formatPermille(starrefCommissionPermille)).string + countOnTop = false + descriptionText = "" } else { titleText = transaction.title ?? " " + countOnTop = false + descriptionText = "" } - descriptionText = "" count = transaction.count - countOnTop = false transactionId = transaction.id date = transaction.date transactionPeer = transaction.peer @@ -443,7 +455,10 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else { switch transaction.peer { case let .peer(peer): - if !transaction.media.isEmpty { + if transaction.flags.contains(.isPaidMessage) { + isPaidMessage = true + titleText = strings.Stars_Transaction_PaidMessage(transaction.paidMessageCount ?? 1) + } else if !transaction.media.isEmpty { titleText = strings.Stars_Transaction_MediaPurchase } else { titleText = transaction.title ?? peer.compactDisplayTitle @@ -613,7 +628,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { } let absCount = StarsAmount(value: abs(count.value), nanos: abs(count.nanos)) - let formattedAmount = presentationStringsFormattedNumber(absCount, dateTimeFormat.groupingSeparator) + let formattedAmount = formatStarsAmountText(absCount, dateTimeFormat: dateTimeFormat) let countColor: UIColor var countFont: UIFont = isSubscription || isSubscriber ? Font.regular(17.0) : Font.semibold(17.0) var countBackgroundColor: UIColor? @@ -735,8 +750,15 @@ private final class StarsTransactionSheetContent: CombinedComponent { transition: .immediate ) } - - let amountAttributedText = NSMutableAttributedString(string: amountText, font: countFont, textColor: countColor) + + let amountAttributedText: NSAttributedString + if amountText.contains(environment.dateTimeFormat.decimalSeparator) { + let smallCountFont = Font.regular(14.0) + amountAttributedText = tonAmountAttributedString(amountText, integralFont: countFont, fractionalFont: smallCountFont, color: countColor, decimalSeparator: environment.dateTimeFormat.decimalSeparator) + } else { + amountAttributedText = NSAttributedString(string: amountText, font: countFont, textColor: countColor) + } + let amount = amount.update( component: BalancedTextComponent( text: .plain(amountAttributedText), @@ -758,6 +780,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) let tableFont = Font.regular(15.0) + let tableBoldFont = Font.semibold(15.0) let tableTextColor = theme.list.itemPrimaryTextColor let tableLinkColor = theme.list.itemAccentColor var tableItems: [TableComponent.Item] = [] @@ -1027,38 +1050,41 @@ private final class StarsTransactionSheetContent: CombinedComponent { } } if let starRefPeerId = transaction.starrefPeerId, let starRefPeer = state.peerMap[starRefPeerId] { - tableItems.append(.init( - id: "to", - title: strings.StarsTransaction_StarRefReason_Affiliate, - component: AnyComponent( - Button( - content: AnyComponent( - PeerCellComponent( - context: component.context, - theme: theme, - peer: starRefPeer - ) - ), - action: { - if delayedCloseOnOpenPeer { - component.openPeer(starRefPeer, false) - Queue.mainQueue().after(1.0, { - component.cancel(false) - }) - } else { - if let controller = controller() as? StarsTransactionScreen, let navigationController = controller.navigationController, let chatController = navigationController.viewControllers.first(where: { $0 is ChatController }) as? ChatController { - chatController.playShakeAnimation() + if !transaction.flags.contains(.isPaidMessage) { + tableItems.append(.init( + id: "to", + title: strings.StarsTransaction_StarRefReason_Affiliate, + component: AnyComponent( + Button( + content: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + peer: starRefPeer + ) + ), + action: { + if delayedCloseOnOpenPeer { + component.openPeer(starRefPeer, false) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } else { + if let controller = controller() as? StarsTransactionScreen, let navigationController = controller.navigationController, let chatController = navigationController.viewControllers.first(where: { $0 is ChatController }) as? ChatController { + chatController.playShakeAnimation() + } + component.cancel(true) } - component.cancel(true) } - } + ) ) - ) - )) + )) + } + if let toPeer { tableItems.append(.init( id: "referred", - title: strings.StarsTransaction_StarRefReason_Referred, + title: transaction.flags.contains(.isPaidMessage) ? strings.Stars_Transaction_From : strings.StarsTransaction_StarRefReason_Referred, component: AnyComponent( Button( content: AnyComponent( @@ -1087,13 +1113,41 @@ private final class StarsTransactionSheetContent: CombinedComponent { } } if let starrefCommissionPermille = transaction.starrefCommissionPermille, transaction.starrefPeerId != nil { - tableItems.append(.init( - id: "commission", - title: strings.StarsTransaction_Commission, - component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "\(formatPermille(starrefCommissionPermille))%", font: tableFont, textColor: tableTextColor)) - )), - insets: UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 5.0) - )) + if transaction.flags.contains(.isPaidMessage) { + var totalStars = transaction.count + if let starrefCount = transaction.starrefAmount { + totalStars = totalStars + starrefCount + } + let valueString = "\(presentationStringsFormattedNumber(abs(Int32(totalStars.value)), dateTimeFormat.groupingSeparator))⭐️" + let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableBoldFont, textColor: theme.list.itemDisclosureActions.constructive.fillColor) + let range = (valueAttributedString.string as NSString).range(of: "⭐️") + if range.location != NSNotFound { + valueAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range) + valueAttributedString.addAttribute(.baselineOffset, value: 1.0, range: range) + } + tableItems.append(.init( + id: "paid", + title: strings.Stars_Transaction_Paid, + component: AnyComponent( + MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: theme.list.mediaPlaceholderColor, + text: .plain(valueAttributedString), + maximumNumberOfLines: 0 + ) + ), + insets: UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 5.0) + )) + } else { + tableItems.append(.init( + id: "commission", + title: strings.StarsTransaction_StarRefReason_Commission, + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "\(formatPermille(starrefCommissionPermille))%", font: tableFont, textColor: tableTextColor)))), + insets: UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 5.0) + )) + } } } @@ -1234,19 +1288,21 @@ private final class StarsTransactionSheetContent: CombinedComponent { var descriptionSize: CGSize = .zero if !descriptionText.isEmpty { let openAppExamples = component.openAppExamples + let openPaidMessageFee = component.openPaidMessageFee if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) } let textColor = countOnTop && !isSubscriber ? theme.list.itemPrimaryTextColor : textColor - let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + 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 attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) } + let descriptionAvailableWidth = isPaidMessage ? context.availableSize.width - sideInset * 2.0 - 16.0 : context.availableSize.width - sideInset * 2.0 - 60.0 let description = description.update( component: MultilineTextComponent( text: .plain(attributedString), @@ -1264,11 +1320,15 @@ private final class StarsTransactionSheetContent: CombinedComponent { }, tapAction: { attributes, _ in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { - openAppExamples() + if isPaidMessage { + openPaidMessageFee() + } else { + openAppExamples() + } } } ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + availableSize: CGSize(width: descriptionAvailableWidth, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) descriptionSize = description.size @@ -1473,6 +1533,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent { let openMessage: (EngineMessage.Id) -> Void let openMedia: ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void let openAppExamples: () -> Void + let openPaidMessageFee: () -> Void let copyTransactionId: (String) -> Void let updateSubscription: () -> Void let sendGift: (EnginePeer.Id) -> Void @@ -1484,6 +1545,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent { openMessage: @escaping (EngineMessage.Id) -> Void, openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void, openAppExamples: @escaping () -> Void, + openPaidMessageFee: @escaping () -> Void, copyTransactionId: @escaping (String) -> Void, updateSubscription: @escaping () -> Void, sendGift: @escaping (EnginePeer.Id) -> Void @@ -1494,6 +1556,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent { self.openMessage = openMessage self.openMedia = openMedia self.openAppExamples = openAppExamples + self.openPaidMessageFee = openPaidMessageFee self.copyTransactionId = copyTransactionId self.updateSubscription = updateSubscription self.sendGift = sendGift @@ -1540,6 +1603,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent { openMessage: context.component.openMessage, openMedia: context.component.openMedia, openAppExamples: context.component.openAppExamples, + openPaidMessageFee: context.component.openPaidMessageFee, copyTransactionId: context.component.copyTransactionId, updateSubscription: context.component.updateSubscription, sendGift: context.component.sendGift @@ -1640,6 +1704,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { var openMessageImpl: ((EngineMessage.Id) -> Void)? var openMediaImpl: (([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? var openAppExamplesImpl: (() -> Void)? + var openPaidMessageFeeImpl: (() -> Void)? var copyTransactionIdImpl: ((String) -> Void)? var updateSubscriptionImpl: (() -> Void)? var sendGiftImpl: ((EnginePeer.Id) -> Void)? @@ -1661,6 +1726,9 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { openAppExamples: { openAppExamplesImpl?() }, + openPaidMessageFee: { + openPaidMessageFeeImpl?() + }, copyTransactionId: { transactionId in copyTransactionIdImpl?(transactionId) }, @@ -1773,6 +1841,20 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { }) } + openPaidMessageFeeImpl = { [weak self] in + guard let self, let navigationController = self.navigationController as? NavigationController else { + return + } + self.dismissAnimated() + let _ = (context.engine.privacy.requestAccountPrivacySettings() + |> deliverOnMainQueue).start(next: { [weak navigationController] privacySettings in + let controller = context.sharedContext.makeIncomingMessagePrivacyScreen(context: context, value: privacySettings.globalSettings.nonContactChatsPrivacy, exceptions: privacySettings.noPaidMessages, update: { settingValue in + let _ = context.engine.privacy.updateNonContactChatsPrivacy(value: settingValue).start() + }) + navigationController?.pushViewController(controller) + }) + } + copyTransactionIdImpl = { [weak self] transactionId in guard let self else { return diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift index 9376b2fcd3..e50a79549d 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift @@ -140,12 +140,20 @@ final class StarsBalanceComponent: Component { let sideInset: CGFloat = 16.0 var contentHeight: CGFloat = sideInset - let balanceString = presentationStringsFormattedNumber(component.count, component.dateTimeFormat.groupingSeparator) + let formattedLabel = formatStarsAmountText(component.count, dateTimeFormat: component.dateTimeFormat) + let labelFont: UIFont + if formattedLabel.contains(component.dateTimeFormat.decimalSeparator) { + labelFont = Font.with(size: 48.0, design: .round, weight: .semibold) + } else { + labelFont = Font.with(size: 48.0, design: .round, weight: .semibold) + } + let smallLabelFont = Font.with(size: 32.0, design: .round, weight: .regular) + let balanceString = tonAmountAttributedString(formattedLabel, integralFont: labelFont, fractionalFont: smallLabelFont, color: component.theme.list.itemPrimaryTextColor, decimalSeparator: component.dateTimeFormat.decimalSeparator) let titleSize = self.title.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: balanceString, font: Font.with(size: 48.0, design: .round, weight: .semibold), textColor: component.theme.list.itemPrimaryTextColor)) + text: .plain(balanceString) ) ), environment: {}, diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index f8f61409c3..935fb0a817 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -17,6 +17,7 @@ import BundleIconComponent import PhotoResources import StarsAvatarComponent import GiftAnimationComponent +import TelegramStringFormatting private extension StarsContext.State.Transaction { var extendedId: String { @@ -303,7 +304,10 @@ final class StarsTransactionsListPanelComponent: Component { var uniqueGift: StarGift.UniqueGift? switch item.peer { case let .peer(peer): - if let starGift = item.starGift { + if item.flags.contains(.isPaidMessage) { + itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) + 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)" itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftUpgrade @@ -380,16 +384,12 @@ final class StarsTransactionsListPanelComponent: Component { } let itemLabel: NSAttributedString - let labelString: String + let formattedLabel = formatStarsAmountText(item.count, dateTimeFormat: environment.dateTimeFormat, showPlus: true) - let absCount = StarsAmount(value: abs(item.count.value), nanos: abs(item.count.nanos)) - let formattedLabel = presentationStringsFormattedNumber(absCount, environment.dateTimeFormat.groupingSeparator) - if item.count < StarsAmount.zero { - labelString = "- \(formattedLabel)" - } else { - labelString = "+ \(formattedLabel)" - } - itemLabel = NSAttributedString(string: labelString, font: Font.medium(fontBaseDisplaySize), textColor: labelString.hasPrefix("-") ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemDisclosureActions.constructive.fillColor) + let smallLabelFont = Font.with(size: floor(fontBaseDisplaySize / 17.0 * 13.0)) + let labelFont = Font.medium(fontBaseDisplaySize) + let labelColor = formattedLabel.hasPrefix("-") ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemDisclosureActions.constructive.fillColor + itemLabel = tonAmountAttributedString(formattedLabel, integralFont: labelFont, fractionalFont: smallLabelFont, color: labelColor, decimalSeparator: environment.dateTimeFormat.decimalSeparator) var itemDateColor = environment.theme.list.itemSecondaryTextColor itemDate = stringForMediumCompactDate(timestamp: item.date, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 54bf286d24..d705156617 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -526,14 +526,15 @@ final class StarsTransactionsScreenComponent: Component { containerSize: CGSize(width: 120.0, height: 100.0) ) + let formattedBalance = formatStarsAmountText(self.starsState?.balance ?? StarsAmount.zero, dateTimeFormat: environment.dateTimeFormat) + let smallLabelFont = Font.regular(11.0) + let labelFont = Font.semibold(14.0) + let balanceText = tonAmountAttributedString(formattedBalance, integralFont: labelFont, fractionalFont: smallLabelFont, color: environment.theme.actionSheet.primaryTextColor, decimalSeparator: environment.dateTimeFormat.decimalSeparator) + let topBalanceValueSize = self.topBalanceValueView.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: presentationStringsFormattedNumber(self.starsState?.balance ?? StarsAmount.zero, environment.dateTimeFormat.groupingSeparator), - font: Font.semibold(14.0), - textColor: environment.theme.actionSheet.primaryTextColor - )), + text: .plain(balanceText), maximumNumberOfLines: 1 )), environment: {}, diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 921272ce37..190070b927 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -124,9 +124,9 @@ private final class SheetContent: CombinedComponent { minAmount = StarsAmount(value: 1, nanos: 0) maxAmount = configuration.maxPaidMediaAmount.flatMap { StarsAmount(value: $0, nanos: 0) } - var usdRate = 0.012 + if let usdWithdrawRate = configuration.usdWithdrawRate, let amount = state.amount, amount > StarsAmount.zero { - usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0 + let usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0 amountLabel = "≈\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))" } else { amountLabel = nil diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index c923c845a9..540e1b1bc4 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -98,6 +98,7 @@ swift_library( "//submodules/TelegramUI/Components/SliderContextItem", "//submodules/TelegramUI/Components/InteractiveTextComponent", "//submodules/TelegramUI/Components/SaveProgressScreen", + "//submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index dabc123237..3c41e12feb 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -219,7 +219,8 @@ public final class StoryContentContextImpl: StoryContentContext { isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, preferHighQualityStories: preferHighQualityStories, boostsToUnrestrict: nil, - appliedBoosts: nil + appliedBoosts: nil, + sendPaidMessageStars: cachedUserData.sendPaidMessageStars ) } else if let cachedChannelData = cachedPeerDataView.cachedPeerData as? CachedChannelData { additionalPeerData = StoryContentContextState.AdditionalPeerData( @@ -230,7 +231,8 @@ public final class StoryContentContextImpl: StoryContentContext { isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, preferHighQualityStories: preferHighQualityStories, boostsToUnrestrict: cachedChannelData.boostsToUnrestrict, - appliedBoosts: cachedChannelData.appliedBoosts + appliedBoosts: cachedChannelData.appliedBoosts, + sendPaidMessageStars: cachedChannelData.sendPaidMessageStars ) } else { additionalPeerData = StoryContentContextState.AdditionalPeerData( @@ -241,7 +243,8 @@ public final class StoryContentContextImpl: StoryContentContext { isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, preferHighQualityStories: preferHighQualityStories, boostsToUnrestrict: nil, - appliedBoosts: nil + appliedBoosts: nil, + sendPaidMessageStars: nil ) } } else { @@ -253,7 +256,8 @@ public final class StoryContentContextImpl: StoryContentContext { isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, preferHighQualityStories: preferHighQualityStories, boostsToUnrestrict: nil, - appliedBoosts: nil + appliedBoosts: nil, + sendPaidMessageStars: nil ) } let state = stateView.value?.get(Stories.PeerState.self) @@ -1182,7 +1186,8 @@ public final class SingleStoryContentContextImpl: StoryContentContext { TelegramEngine.EngineData.Item.NotificationSettings.Global(), TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging(id: storyId.peerId), TelegramEngine.EngineData.Item.Peer.BoostsToUnrestrict(id: storyId.peerId), - TelegramEngine.EngineData.Item.Peer.AppliedBoosts(id: storyId.peerId) + TelegramEngine.EngineData.Item.Peer.AppliedBoosts(id: storyId.peerId), + TelegramEngine.EngineData.Item.Peer.SendPaidMessageStars(id: storyId.peerId) ), item |> mapToSignal { item -> Signal<(Stories.StoredItem?, [PeerId: Peer], [MediaId: TelegramMediaFile], [StoryId: EngineStoryItem?]), NoError> in return context.account.postbox.transaction { transaction -> (Stories.StoredItem?, [PeerId: Peer], [MediaId: TelegramMediaFile], [StoryId: EngineStoryItem?]) in @@ -1253,7 +1258,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext { return } - let (peer, presence, areVoiceMessagesAvailable, canViewStats, notificationSettings, globalNotificationSettings, isPremiumRequiredForMessaging, boostsToUnrestrict, appliedBoosts) = data + let (peer, presence, areVoiceMessagesAvailable, canViewStats, notificationSettings, globalNotificationSettings, isPremiumRequiredForMessaging, boostsToUnrestrict, appliedBoosts, sendPaidMessageStars) = data let (item, peers, allEntityFiles, forwardInfoStories) = itemAndPeers guard let peer else { @@ -1270,7 +1275,8 @@ public final class SingleStoryContentContextImpl: StoryContentContext { isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, preferHighQualityStories: preferHighQualityStories, boostsToUnrestrict: boostsToUnrestrict, - appliedBoosts: appliedBoosts + appliedBoosts: appliedBoosts, + sendPaidMessageStars: sendPaidMessageStars ) for (storyId, story) in forwardInfoStories { @@ -1436,9 +1442,11 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { TelegramEngine.EngineData.Item.NotificationSettings.Global.Result, TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging.Result, TelegramEngine.EngineData.Item.Peer.BoostsToUnrestrict.Result, - TelegramEngine.EngineData.Item.Peer.AppliedBoosts.Result) + TelegramEngine.EngineData.Item.Peer.AppliedBoosts.Result, + TelegramEngine.EngineData.Item.Peer.SendPaidMessageStars.Result + ) - init(data: (TelegramEngine.EngineData.Item.Peer.Peer.Result, TelegramEngine.EngineData.Item.Peer.Presence.Result, TelegramEngine.EngineData.Item.Peer.AreVoiceMessagesAvailable.Result, TelegramEngine.EngineData.Item.Peer.CanViewStats.Result, TelegramEngine.EngineData.Item.Peer.NotificationSettings.Result, TelegramEngine.EngineData.Item.NotificationSettings.Global.Result, TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging.Result, TelegramEngine.EngineData.Item.Peer.BoostsToUnrestrict.Result, TelegramEngine.EngineData.Item.Peer.AppliedBoosts.Result)) { + init(data: (TelegramEngine.EngineData.Item.Peer.Peer.Result, TelegramEngine.EngineData.Item.Peer.Presence.Result, TelegramEngine.EngineData.Item.Peer.AreVoiceMessagesAvailable.Result, TelegramEngine.EngineData.Item.Peer.CanViewStats.Result, TelegramEngine.EngineData.Item.Peer.NotificationSettings.Result, TelegramEngine.EngineData.Item.NotificationSettings.Global.Result, TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging.Result, TelegramEngine.EngineData.Item.Peer.BoostsToUnrestrict.Result, TelegramEngine.EngineData.Item.Peer.AppliedBoosts.Result, TelegramEngine.EngineData.Item.Peer.SendPaidMessageStars.Result)) { self.data = data } } @@ -1544,7 +1552,8 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { TelegramEngine.EngineData.Item.NotificationSettings.Global(), TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging(id: peerId), TelegramEngine.EngineData.Item.Peer.BoostsToUnrestrict(id: peerId), - TelegramEngine.EngineData.Item.Peer.AppliedBoosts(id: peerId) + TelegramEngine.EngineData.Item.Peer.AppliedBoosts(id: peerId), + TelegramEngine.EngineData.Item.Peer.SendPaidMessageStars(id: peerId) ) |> map { PeerData(data: $0) }) self.currentPeerData = currentPeerData @@ -1563,7 +1572,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { self.listState = state let stateValue: StoryContentContextState - if let focusedIndex, let (peer, presence, areVoiceMessagesAvailable, canViewStats, notificationSettings, globalNotificationSettings, isPremiumRequiredForMessaging, boostsToUnrestrict, appliedBoosts) = data?.data, let peer { + if let focusedIndex, let (peer, presence, areVoiceMessagesAvailable, canViewStats, notificationSettings, globalNotificationSettings, isPremiumRequiredForMessaging, boostsToUnrestrict, appliedBoosts, sendPaidMessageStars) = data?.data, let peer { let isMuted = resolvedAreStoriesMuted(globalSettings: globalNotificationSettings._asGlobalNotificationSettings(), peer: peer._asPeer(), peerSettings: notificationSettings._asNotificationSettings(), topSearchPeers: []) let additionalPeerData = StoryContentContextState.AdditionalPeerData( isMuted: isMuted, @@ -1573,7 +1582,8 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, preferHighQualityStories: preferHighQualityStories, boostsToUnrestrict: boostsToUnrestrict, - appliedBoosts: appliedBoosts + appliedBoosts: appliedBoosts, + sendPaidMessageStars: sendPaidMessageStars ) let item = state.items[focusedIndex] @@ -2462,7 +2472,8 @@ public final class RepostStoriesContentContextImpl: StoryContentContext { isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, preferHighQualityStories: preferHighQualityStories, boostsToUnrestrict: nil, - appliedBoosts: nil + appliedBoosts: nil, + sendPaidMessageStars: cachedUserData.sendPaidMessageStars ) } else if let cachedChannelData = cachedPeerDataView.cachedPeerData as? CachedChannelData { additionalPeerData = StoryContentContextState.AdditionalPeerData( @@ -2473,7 +2484,8 @@ public final class RepostStoriesContentContextImpl: StoryContentContext { isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, preferHighQualityStories: preferHighQualityStories, boostsToUnrestrict: cachedChannelData.boostsToUnrestrict, - appliedBoosts: cachedChannelData.appliedBoosts + appliedBoosts: cachedChannelData.appliedBoosts, + sendPaidMessageStars: cachedChannelData.sendPaidMessageStars ) } else { additionalPeerData = StoryContentContextState.AdditionalPeerData( @@ -2484,7 +2496,8 @@ public final class RepostStoriesContentContextImpl: StoryContentContext { isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, preferHighQualityStories: preferHighQualityStories, boostsToUnrestrict: nil, - appliedBoosts: nil + appliedBoosts: nil, + sendPaidMessageStars: nil ) } } @@ -2497,7 +2510,8 @@ public final class RepostStoriesContentContextImpl: StoryContentContext { isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, preferHighQualityStories: preferHighQualityStories, boostsToUnrestrict: nil, - appliedBoosts: nil + appliedBoosts: nil, + sendPaidMessageStars: nil ) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift index 90479b4c0e..b4e4c11784 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift @@ -161,6 +161,7 @@ public final class StoryContentContextState { public let preferHighQualityStories: Bool public let boostsToUnrestrict: Int32? public let appliedBoosts: Int32? + public let sendPaidMessageStars: StarsAmount? public init( isMuted: Bool, @@ -170,7 +171,8 @@ public final class StoryContentContextState { isPremiumRequiredForMessaging: Bool, preferHighQualityStories: Bool, boostsToUnrestrict: Int32?, - appliedBoosts: Int32? + appliedBoosts: Int32?, + sendPaidMessageStars: StarsAmount? ) { self.isMuted = isMuted self.areVoiceMessagesAvailable = areVoiceMessagesAvailable @@ -180,6 +182,7 @@ public final class StoryContentContextState { self.preferHighQualityStories = preferHighQualityStories self.boostsToUnrestrict = boostsToUnrestrict self.appliedBoosts = appliedBoosts + self.sendPaidMessageStars = sendPaidMessageStars } public static func == (lhs: StoryContentContextState.AdditionalPeerData, rhs: StoryContentContextState.AdditionalPeerData) -> Bool { @@ -207,6 +210,9 @@ public final class StoryContentContextState { if lhs.appliedBoosts != rhs.appliedBoosts { return false } + if lhs.sendPaidMessageStars != rhs.sendPaidMessageStars { + return false + } return true } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 97f1f3c842..2f0f3c2f74 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -2823,7 +2823,12 @@ public final class StoryItemSetContainerComponent: Component { inputPlaceholder = .counter(items) } else { - inputPlaceholder = .plain(isGroup ? component.strings.Story_InputPlaceholderReplyInGroup : component.strings.Story_InputPlaceholderReplyPrivately) + if let sendPaidMessageStars = component.slice.additionalPeerData.sendPaidMessageStars { + let dateTimeFormat = component.context.sharedContext.currentPresentationData.with { $0 }.dateTimeFormat + inputPlaceholder = .plain(component.strings.Chat_InputTextPaidMessagePlaceholder(" # \(presentationStringsFormattedNumber(Int32(sendPaidMessageStars.value), dateTimeFormat.groupingSeparator))").string) + } else { + inputPlaceholder = .plain(isGroup ? component.strings.Story_InputPlaceholderReplyInGroup : component.strings.Story_InputPlaceholderReplyPrivately) + } } let startTime22 = CFAbsoluteTimeGetCurrent() @@ -2867,6 +2872,7 @@ public final class StoryItemSetContainerComponent: Component { strings: component.strings, style: .story, placeholder: inputPlaceholder, + sendPaidMessageStars: component.slice.additionalPeerData.sendPaidMessageStars, maxLength: 4096, queryTypes: [.mention, .hashtag, .emoji], alwaysDarkWhenHasText: component.metrics.widthClass == .regular, @@ -4647,6 +4653,10 @@ public final class StoryItemSetContainerComponent: Component { case .stars: break } + + if let sendPaidMessageStars = component.slice.additionalPeerData.sendPaidMessageStars { + messageAttributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: false)) + } let message: EnqueueMessage = .message( text: text, @@ -4693,7 +4703,6 @@ public final class StoryItemSetContainerComponent: Component { }) } } - if self.displayLikeReactions { if component.slice.item.storyItem.myReaction == updateReaction.reaction { action() @@ -4704,7 +4713,9 @@ public final class StoryItemSetContainerComponent: Component { } } else { self.sendMessageContext.performWithPossibleStealthModeConfirmation(view: self, action: { - action() + self.sendMessageContext.presentPaidMessageAlertIfNeeded(view: self, completion: { + action() + }) }) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 02b9a3db5f..7140300641 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -50,6 +50,7 @@ import LocationUI import ReactionSelectionNode import StoryQualityUpgradeSheetScreen import AudioWaveform +import ChatMessagePaymentAlertController private var ObjCKey_DeinitWatcher: Int? @@ -491,6 +492,30 @@ final class StoryItemSetContainerSendMessage { view.updateIsProgressPaused() } + func presentPaidMessageAlertIfNeeded(view: StoryItemSetContainerComponent.View, completion: @escaping () -> Void) { + guard let component = view.component, let sendPaidMessageStars = component.slice.additionalPeerData.sendPaidMessageStars else { + completion() + return + } + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme) + + let controller = chatMessagePaymentAlertController( + context: component.context, + presentationData: presentationData, + updatedPresentationData: nil, + peers: [component.slice.effectivePeer], + count: 1, + amount: sendPaidMessageStars, + totalAmount: nil, + hasCheck: false, + navigationController: component.controller()?.navigationController as? NavigationController, + completion: { _ in + completion() + } + ) + component.controller()?.present(controller, in: .window(.root)) + } + func performWithPossibleStealthModeConfirmation(view: StoryItemSetContainerComponent.View, action: @escaping () -> Void) { guard let component = view.component, component.stealthModeTimeout != nil else { action() @@ -512,7 +537,6 @@ final class StoryItemSetContainerSendMessage { let timestamp = Int32(Date().timeIntervalSince1970) if noticeCount < 1, let activeUntilTimestamp = config.stealthModeState.actualizedNow().activeUntilTimestamp, activeUntilTimestamp > timestamp { - let theme = component.theme let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) @@ -575,53 +599,64 @@ final class StoryItemSetContainerSendMessage { let controller = component.controller() as? StoryContainerScreen - if let recordedAudioPreview = self.recordedAudioPreview, case let .audio(audio) = recordedAudioPreview { - self.recordedAudioPreview = nil - - let waveformBuffer = audio.waveform.makeBitstream() - - let messages: [EnqueueMessage] = [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: audio.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(audio.fileSize), attributes: [.Audio(isVoice: true, duration: Int(audio.duration), title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] - - let _ = enqueueMessages(account: component.context.account, peerId: peerId, messages: messages).start() - - view.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) - } else if self.hasRecordedVideoPreview, let videoRecorderValue = self.videoRecorderValue { - videoRecorderValue.send() - self.hasRecordedVideoPreview = false - self.videoRecorder.set(.single(nil)) - view.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) - } else { - switch inputPanelView.getSendMessageInput() { - case let .text(text): - if !text.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - let entities = generateChatInputTextEntities(text) - let _ = (component.context.engine.messages.enqueueOutgoingMessage( - to: peerId, - replyTo: nil, - storyId: focusedStoryId, - content: .text(text.string, entities), - silentPosting: silentPosting, - scheduleTime: scheduleTime - ) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in - Queue.mainQueue().after(0.3) { - if let self, let view { - self.presentMessageSentTooltip(view: view, peer: peer, messageId: messageIds.first.flatMap { $0 }, isScheduled: scheduleTime != nil) + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self, weak view] in + guard let self, let view else { + return + } + if let recordedAudioPreview = self.recordedAudioPreview, case let .audio(audio) = recordedAudioPreview { + self.recordedAudioPreview = nil + + let waveformBuffer = audio.waveform.makeBitstream() + + var messageAttributes: [MessageAttribute] = [] + if let sendPaidMessageStars = component.slice.additionalPeerData.sendPaidMessageStars { + messageAttributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: false)) + } + + let messages: [EnqueueMessage] = [.message(text: "", attributes: messageAttributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: audio.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(audio.fileSize), attributes: [.Audio(isVoice: true, duration: Int(audio.duration), title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] + + let _ = enqueueMessages(account: component.context.account, peerId: peerId, messages: messages).start() + + view.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) + } else if self.hasRecordedVideoPreview, let videoRecorderValue = self.videoRecorderValue { + videoRecorderValue.send() + self.hasRecordedVideoPreview = false + self.videoRecorder.set(.single(nil)) + view.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) + } else { + switch inputPanelView.getSendMessageInput() { + case let .text(text): + if !text.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + let entities = generateChatInputTextEntities(text) + let _ = (component.context.engine.messages.enqueueOutgoingMessage( + to: peerId, + replyTo: nil, + storyId: focusedStoryId, + content: .text(text.string, entities), + silentPosting: silentPosting, + scheduleTime: scheduleTime, + sendPaidMessageStars: component.slice.additionalPeerData.sendPaidMessageStars + ) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in + Queue.mainQueue().after(0.3) { + if let self, let view { + self.presentMessageSentTooltip(view: view, peer: peer, messageId: messageIds.first.flatMap { $0 }, isScheduled: scheduleTime != nil) + } } + }) + component.storyItemSharedState.replyDrafts.removeValue(forKey: StoryId(peerId: peerId, id: focusedItem.storyItem.id)) + inputPanelView.clearSendMessageInput(updateState: true) + + self.currentInputMode = .text + if hasFirstResponder(view) { + view.endEditing(true) + } else { + view.state?.updated(transition: .spring(duration: 0.3)) } - }) - component.storyItemSharedState.replyDrafts.removeValue(forKey: StoryId(peerId: peerId, id: focusedItem.storyItem.id)) - inputPanelView.clearSendMessageInput(updateState: true) - - self.currentInputMode = .text - if hasFirstResponder(view) { - view.endEditing(true) - } else { - view.state?.updated(transition: .spring(duration: 0.3)) + controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring)) } - controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring)) } } - } + }) }) } @@ -660,26 +695,32 @@ final class StoryItemSetContainerSendMessage { }) } - let _ = (component.context.engine.messages.enqueueOutgoingMessage( - to: peerId, - replyTo: nil, - storyId: focusedStoryId, - content: .file(fileReference) - ) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in - Queue.mainQueue().after(0.3) { - if let self, let view { - self.presentMessageSentTooltip(view: view, peer: peer, messageId: messageIds.first.flatMap { $0 }) - } + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self, weak view] in + guard let self, let view else { + return } + let _ = (component.context.engine.messages.enqueueOutgoingMessage( + to: peerId, + replyTo: nil, + storyId: focusedStoryId, + content: .file(fileReference), + sendPaidMessageStars: component.slice.additionalPeerData.sendPaidMessageStars + ) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in + Queue.mainQueue().after(0.3) { + if let self, let view { + self.presentMessageSentTooltip(view: view, peer: peer, messageId: messageIds.first.flatMap { $0 }) + } + } + }) + + self.currentInputMode = .text + if hasFirstResponder(view) { + view.endEditing(true) + } else { + view.state?.updated(transition: .spring(duration: 0.3)) + } + controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring)) }) - - self.currentInputMode = .text - if hasFirstResponder(view) { - view.endEditing(true) - } else { - view.state?.updated(transition: .spring(duration: 0.3)) - } - controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring)) }) } @@ -714,37 +755,48 @@ final class StoryItemSetContainerSendMessage { }) } - let _ = (component.context.engine.messages.enqueueOutgoingMessage( - to: peerId, - replyTo: nil, - storyId: focusedStoryId, - content: .contextResult(results, result) - ) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in - Queue.mainQueue().after(0.3) { - if let self, let view { - self.presentMessageSentTooltip(view: view, peer: peer, messageId: messageIds.first.flatMap { $0 }) - } + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self, weak view] in + guard let self, let view else { + return } + let _ = (component.context.engine.messages.enqueueOutgoingMessage( + to: peerId, + replyTo: nil, + storyId: focusedStoryId, + content: .contextResult(results, result), + sendPaidMessageStars: component.slice.additionalPeerData.sendPaidMessageStars + ) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in + Queue.mainQueue().after(0.3) { + if let self, let view { + self.presentMessageSentTooltip(view: view, peer: peer, messageId: messageIds.first.flatMap { $0 }) + } + } + }) + + self.currentInputMode = .text + if hasFirstResponder(view) { + view.endEditing(true) + } else { + view.state?.updated(transition: .spring(duration: 0.3)) + } + controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring)) }) - - self.currentInputMode = .text - if hasFirstResponder(view) { - view.endEditing(true) - } else { - view.state?.updated(transition: .spring(duration: 0.3)) - } - controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring)) } func enqueueGifData(view: StoryItemSetContainerComponent.View, data: Data) { guard let component = view.component else { return } - let peer = component.slice.effectivePeer - let _ = (legacyEnqueueGifMessage(account: component.context.account, data: data) |> deliverOnMainQueue).start(next: { [weak self, weak view] message in - if let self, let view { - self.sendMessages(view: view, peer: peer, messages: [message]) + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self] in + guard let self else { + return } + let peer = component.slice.effectivePeer + let _ = (legacyEnqueueGifMessage(account: component.context.account, data: data) |> deliverOnMainQueue).start(next: { [weak self, weak view] message in + if let self, let view { + self.sendMessages(view: view, peer: peer, messages: [message]) + } + }) }) } @@ -794,7 +846,12 @@ final class StoryItemSetContainerSendMessage { let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: Int64(data.count), attributes: fileAttributes, alternativeRepresentations: []) let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - self.sendMessages(view: view, peer: peer, messages: [message], silentPosting: false) + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self] in + guard let self else { + return + } + self.sendMessages(view: view, peer: peer, messages: [message], silentPosting: false) + }) } }) } @@ -846,7 +903,12 @@ final class StoryItemSetContainerSendMessage { guard let self, let view else { return } - self.sendMessages(view: view, peer: peer, messages: [updatedMessage]) + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self, weak view] in + guard let self, let view else { + return + } + self.sendMessages(view: view, peer: peer, messages: [updatedMessage]) + }) }) }, displaySlowmodeTooltip: { [weak self] view, rect in //self?.interfaceInteraction?.displaySlowmodeTooltip(view, rect) @@ -896,9 +958,14 @@ final class StoryItemSetContainerSendMessage { guard let self, let view else { return } - self.sendMessages(view: view, peer: peer, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) - - HapticFeedback().tap() + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self, weak view] in + guard let self, let view else { + return + } + self.sendMessages(view: view, peer: peer, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + + HapticFeedback().tap() + }) }) } }) @@ -1593,7 +1660,11 @@ final class StoryItemSetContainerSendMessage { guard let view, let component = view.component else { return } - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + var messageAttributes: [MessageAttribute] = [] + if let sendPaidMessageStars = component.slice.additionalPeerData.sendPaidMessageStars { + messageAttributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: false)) + } + let message: EnqueueMessage = .message(text: "", attributes: messageAttributes, inlineStickers: [:], mediaReference: mediaReference, threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = (enqueueMessages(account: component.context.account, peerId: peer.id, messages: [message.withUpdatedReplyToMessageId(nil)]) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in if let self, let view { @@ -1636,7 +1707,12 @@ final class StoryItemSetContainerSendMessage { return } let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - self.sendMessages(view: view, peer: peer, messages: [message]) + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self] in + guard let self else { + return + } + self.sendMessages(view: view, peer: peer, messages: [message]) + }) }) completion(controller, controller.mediaPickerContext) @@ -1705,7 +1781,12 @@ final class StoryItemSetContainerSendMessage { } } - self.sendMessages(view: view, peer: peer, messages: enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime) + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self] in + guard let self else { + return + } + self.sendMessages(view: view, peer: peer, messages: enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime) + }) } else if let peer = peers.first { let dataSignal: Signal<(EnginePeer?, DeviceContactExtendedData?), NoError> switch peer { @@ -1760,7 +1841,12 @@ final class StoryItemSetContainerSendMessage { } enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) - self.sendMessages(view: view, peer: targetPeer, messages: enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime) + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self] in + guard let self else { + return + } + self.sendMessages(view: view, peer: targetPeer, messages: enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime) + }) } else { let contactController = component.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: component.context), environment: ShareControllerAppEnvironment(sharedContext: component.context.sharedContext), subject: .filter(peer: peerAndContactData.0?._asPeer(), contactId: nil, contactData: contactData, completion: { [weak self, weak view] peer, contactData in guard let self, let view else { @@ -1779,7 +1865,12 @@ final class StoryItemSetContainerSendMessage { } enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) - self.sendMessages(view: view, peer: targetPeer, messages: enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime) + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self] in + guard let self else { + return + } + self.sendMessages(view: view, peer: targetPeer, messages: enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime) + }) } }), completed: nil, cancelled: nil) component.controller()?.push(contactController) @@ -1937,7 +2028,7 @@ final class StoryItemSetContainerSendMessage { } return self.getCaptionPanelView(view: view, peer: peer, mediaPicker: controller) } - controller.legacyCompletion = { signals, silently, scheduleTime, messageEffect, getAnimatedTransitionSource, sendCompletion in + controller.legacyCompletion = { _, signals, silently, scheduleTime, messageEffect, getAnimatedTransitionSource, sendCompletion in completion(signals, silently, scheduleTime, messageEffect, getAnimatedTransitionSource, sendCompletion) } present(controller, mediaPickerContext) @@ -2187,7 +2278,12 @@ final class StoryItemSetContainerSendMessage { } if !messages.isEmpty { - strongSelf.sendMessages(view: view, peer: peer, messages: messages) + strongSelf.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.sendMessages(view: view, peer: peer, messages: messages) + }) } } })) @@ -2248,7 +2344,12 @@ final class StoryItemSetContainerSendMessage { if !inputText.string.isEmpty { self.clearInputText(view: view) } - self.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: nil, replyToStoryId: focusedStoryId, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, parameters: parameters, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self] in + guard let self else { + return + } + self.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: nil, replyToStoryId: focusedStoryId, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, parameters: parameters, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) + }) } ) } @@ -2268,7 +2369,19 @@ final class StoryItemSetContainerSendMessage { guard let self, let view, let component = view.component else { return } - if component.context.engine.messages.enqueueOutgoingMessageWithChatContextResult(to: peer.id, threadId: nil, botId: results.botId, result: result, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: storyId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime) { + if component.context.engine.messages.enqueueOutgoingMessageWithChatContextResult( + to: peer.id, + threadId: nil, + botId: results.botId, + result: result, + replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, + replyToStoryId: storyId, + hideVia: hideVia, + silentPosting: silentPosting, + scheduleTime: scheduleTime, + sendPaidMessageStars: component.slice.additionalPeerData.sendPaidMessageStars, + postpone: false + ) { } if let attachmentController = self.attachmentController { @@ -2416,10 +2529,15 @@ final class StoryItemSetContainerSendMessage { guard let self, let view else { return } - self.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil, parameters: parameters) - if !inputText.string.isEmpty { - self.clearInputText(view: view) - } + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self, weak view] in + guard let self, let view else { + return + } + self.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil, parameters: parameters) + if !inputText.string.isEmpty { + self.clearInputText(view: view) + } + }) }, recognizedQRCode: { _ in }, presentSchedulePicker: { [weak self, weak view] _, done in guard let self, let view else { @@ -2545,6 +2663,10 @@ final class StoryItemSetContainerSendMessage { attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime)) } } + var messageAttributes: [MessageAttribute] = [] + if let component = view.component, let sendPaidMessageStars = component.slice.additionalPeerData.sendPaidMessageStars { + messageAttributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: false)) + } return attributes } } diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index 054b0fc279..76198269d7 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -374,6 +374,7 @@ public class ImmediateTextNodeWithEntities: TextNode { private var linkHighlightingNode: LinkHighlightingNode? public var linkHighlightColor: UIColor? + public var linkHighlightInset: UIEdgeInsets = .zero public var trailingLineWidth: CGFloat? @@ -634,7 +635,7 @@ public class ImmediateTextNodeWithEntities: TextNode { } } - if let rects = rects { + if var rects, !rects.isEmpty { let linkHighlightingNode: LinkHighlightingNode if let current = strongSelf.linkHighlightingNode { linkHighlightingNode = current @@ -644,6 +645,7 @@ public class ImmediateTextNodeWithEntities: TextNode { strongSelf.addSubnode(linkHighlightingNode) } linkHighlightingNode.frame = strongSelf.bounds + rects[rects.count - 1] = rects[rects.count - 1].inset(by: strongSelf.linkHighlightInset) linkHighlightingNode.updateRects(rects.map { $0.offsetBy(dx: 0.0, dy: 0.0) }) } else if let linkHighlightingNode = strongSelf.linkHighlightingNode { strongSelf.linkHighlightingNode = nil diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Perk/PaidMessages.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Perk/PaidMessages.imageset/Contents.json new file mode 100644 index 0000000000..2b5437edde --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Perk/PaidMessages.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "paidmessages.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Perk/PaidMessages.imageset/paidmessages.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Perk/PaidMessages.imageset/paidmessages.pdf new file mode 100644 index 0000000000..e743406c74 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Perk/PaidMessages.imageset/paidmessages.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/ButtonStar.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/ButtonStar.imageset/Contents.json new file mode 100644 index 0000000000..67417e8ee9 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/ButtonStar.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "star (2).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/ButtonStar.imageset/star (2).pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/ButtonStar.imageset/star (2).pdf new file mode 100644 index 0000000000..563c1e5797 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Stars/ButtonStar.imageset/star (2).pdf differ diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 9d865836d5..a8b2117736 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -1237,7 +1237,7 @@ extension ChatControllerImpl { }, messageCorrelationId) } - self.chatDisplayNode.sendMessages = { [weak self] messages, silentPosting, scheduleTime, isAnyMessageTextPartitioned in + self.chatDisplayNode.sendMessages = { [weak self] messages, silentPosting, scheduleTime, isAnyMessageTextPartitioned, postpone in guard let strongSelf = self else { return } @@ -1285,7 +1285,7 @@ extension ChatControllerImpl { } } - let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting ?? false, scheduleTime: scheduleTime) + let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting ?? false, scheduleTime: scheduleTime, postpone: postpone) var forwardedMessages: [[EnqueueMessage]] = [] var forwardSourcePeerIds = Set() @@ -1412,8 +1412,7 @@ extension ChatControllerImpl { strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .succeed(text: strongSelf.presentationData.strings.Business_Links_EditLinkToastSaved, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), in: .current) } } - //TODO:unmock - strongSelf.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false).updatedAcknowledgedPaidMessage(false) }) + strongSelf.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) }) } if case let .customChatContents(customChatContents) = self.subject { @@ -2605,8 +2604,12 @@ extension ChatControllerImpl { strongSelf.interfaceInteraction?.displaySlowmodeTooltip(node.view, rect) return false } - - strongSelf.enqueueChatContextResult(results, result) + strongSelf.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in + guard let strongSelf = self else { + return + } + strongSelf.enqueueChatContextResult(results, result, postpone: postpone) + }) return true }, sendBotCommand: { [weak self] botPeer, command in if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) { @@ -2856,7 +2859,9 @@ extension ChatControllerImpl { }, deleteRecordedMedia: { [weak self] in self?.deleteMediaRecording() }, sendRecordedMedia: { [weak self] silentPosting, viewOnce in - self?.sendMediaRecording(silentPosting: silentPosting, viewOnce: viewOnce) + self?.presentPaidMessageAlertIfNeeded(count: 1, completion: { [weak self] postpone in + self?.sendMediaRecording(silentPosting: silentPosting, viewOnce: viewOnce, postpone: postpone) + }) }, displayRestrictedInfo: { [weak self] subject, displayType in guard let strongSelf = self else { return @@ -4397,30 +4402,8 @@ extension ChatControllerImpl { }) self.push(controller) }) - }, openMessagePayment: { [weak self] in - guard let self, let peer = self.presentationInterfaceState.renderedPeer?.peer.flatMap(EnginePeer.init) else { - return - } - var amount: StarsAmount? - if let cachedUserData = self.peerView?.cachedData as? CachedUserData { - amount = cachedUserData.sendPaidMessageStars - } - if let amount { - let controller = chatMessagePaymentAlertController( - context: self.context, - updatedPresentationData: self.updatedPresentationData, - peer: peer, - amount: amount, - completion: { [weak self] dontAskAgain in - guard let self else { - return - } - self.updateChatPresentationInterfaceState(interactive: true) { state in - return state.updatedAcknowledgedPaidMessage(true) - } - }) - self.present(controller, in: .window(.root)) - } + }, 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 { return diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift index ac4358c04a..455ea29bdf 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift @@ -156,7 +156,7 @@ extension ChatControllerImpl { var viewOnceAvailable = false if let peerId = self.chatLocation.peerId { allowLiveUpload = peerId.namespace != Namespaces.Peer.SecretChat - viewOnceAvailable = !isScheduledMessages && peerId.namespace == Namespaces.Peer.CloudUser && peerId != self.context.account.peerId && !isBot + viewOnceAvailable = !isScheduledMessages && peerId.namespace == Namespaces.Peer.CloudUser && peerId != self.context.account.peerId && !isBot && self.presentationInterfaceState.sendPaidMessageStars == nil } else if case .customChatContents = self.chatLocation { allowLiveUpload = true } @@ -249,6 +249,12 @@ extension ChatControllerImpl { updatedAction = .preview } + var sendImmediately = false + if let _ = self.presentationInterfaceState.sendPaidMessageStars, case .send = action { + updatedAction = .preview + sendImmediately = true + } + if let audioRecorderValue = self.audioRecorderValue { switch action { case .pause: @@ -296,6 +302,10 @@ extension ChatControllerImpl { strongSelf.recorderFeedback = nil strongSelf.updateDownButtonVisibility() strongSelf.recorderDataDisposable.set(nil) + + if sendImmediately { + strongSelf.interfaceInteraction?.sendRecordedMedia(false, false) + } } } })) @@ -476,7 +486,7 @@ extension ChatControllerImpl { self.updateDownButtonVisibility() } - func sendMediaRecording(silentPosting: Bool? = nil, scheduleTime: Int32? = nil, viewOnce: Bool = false, messageEffect: ChatSendMessageEffect? = nil) { + func sendMediaRecording(silentPosting: Bool? = nil, scheduleTime: Int32? = nil, viewOnce: Bool = false, messageEffect: ChatSendMessageEffect? = nil, postpone: Bool = false) { self.chatDisplayNode.updateRecordedMediaDeleted(false) guard let recordedMediaPreview = self.presentationInterfaceState.interfaceState.mediaDraftState else { @@ -525,9 +535,9 @@ extension ChatControllerImpl { let transformedMessages: [EnqueueMessage] if let silentPosting = silentPosting { - transformedMessages = self.transformEnqueueMessages(messages, silentPosting: silentPosting) + transformedMessages = self.transformEnqueueMessages(messages, silentPosting: silentPosting, postpone: postpone) } else if let scheduleTime = scheduleTime { - transformedMessages = self.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime) + transformedMessages = self.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime, postpone: postpone) } else { transformedMessages = self.transformEnqueueMessages(messages) } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift new file mode 100644 index 0000000000..0e1b9de5e6 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift @@ -0,0 +1,126 @@ +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 presentPaidMessageAlertIfNeeded(count: Int32 = 1, forceDark: Bool = false, completion: @escaping (Bool) -> Void) { + guard let peer = self.presentationInterfaceState.renderedPeer?.peer.flatMap(EnginePeer.init) else { + completion(false) + return + } + if let sendPaidMessageStars = self.presentationInterfaceState.sendPaidMessageStars { + let _ = (ApplicationSpecificNotice.dismissedPaidMessageWarningNamespace(accountManager: self.context.sharedContext.accountManager, peerId: peer.id) + |> 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 > sendPaidMessageStars { + completion(true) + self.displayPaidMessageUndo(count: count, amount: sendPaidMessageStars) + } else { + var presentationData = self.presentationData + if forceDark { + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } + let controller = chatMessagePaymentAlertController( + context: self.context, + presentationData: presentationData, + updatedPresentationData: nil, + peers: [peer], + count: count, + amount: sendPaidMessageStars, + totalAmount: nil, + navigationController: self.navigationController as? NavigationController, + completion: { [weak self] dontAskAgain in + guard let self else { + return + } + + if dontAskAgain { + let _ = ApplicationSpecificNotice.setDismissedPaidMessageWarningNamespace(accountManager: self.context.sharedContext.accountManager, peerId: peer.id, amount: sendPaidMessageStars.value).start() + } + + if let currentState = starsContext.currentState, currentState.balance < sendPaidMessageStars { + let _ = (self.context.engine.payments.starsTopUpOptions() + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] options in + guard let self else { + return + } + let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: options, purpose: .sendMessage(peerId: peer.id, requiredStars: sendPaidMessageStars.value), completion: { _ in + completion(false) + }) + self.push(controller) + }) + } else { + completion(false) + } + } + ) + self.present(controller, in: .window(.root)) + } + }) + } else { + completion(false) + } + } + + func displayPaidMessageUndo(count: Int32, amount: StarsAmount) { + guard let peerId = self.chatLocation.peerId else { + return + } + + if let current = self.currentPaidMessageUndoController { + self.currentPaidMessageUndoController = nil + current.dismiss() + + self.context.engine.messages.forceSendPostponedPaidMessage(peerId: peerId) + } + + //TODO:localize + let title: String + if count > 1 { + title = "\(count) Messages Sent" + } else { + title = "Message Sent" + } + + let textItems: [AnimatedTextComponent.Item] = [ + AnimatedTextComponent.Item(id: 0, content: .text("You paid \(amount.value * Int64(count)) Stars")) + ] + + let controller = UndoOverlayController(presentationData: self.presentationData, content: .starsSent(context: self.context, title: title, text: textItems), elevatedLayout: false, position: .top, action: { [weak self] action in + guard let self else { + return false + } + if case .undo = action { + var messageIds: [MessageId] = [] + self.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemNodeProtocol { + for message in itemNode.messages() { + if message.id.namespace == Namespaces.Message.Local { + messageIds.append(message.id) + } + } + } + } + let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: messageIds, type: .forLocalPeer).startStandalone() + } + return false + }) + self.currentPaidMessageUndoController = controller + self.present(controller, in: .current) + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift index b30a0fc987..faded6689d 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift @@ -28,8 +28,8 @@ extension ChatControllerImpl { subjects: subjects, presentMediaPicker: { [weak self] subject, saveEditedPhotos, bannedSendPhotos, bannedSendVideos, present in if let strongSelf = self { - strongSelf.presentMediaPicker(subject: subject, saveEditedPhotos: saveEditedPhotos, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, present: present, updateMediaPickerContext: { _ in }, completion: { [weak self] signals, silentPosting, scheduleTime, parameters, getAnimatedTransitionSource, completion in - self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, parameters: parameters, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) + strongSelf.presentMediaPicker(subject: subject, saveEditedPhotos: saveEditedPhotos, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, present: present, updateMediaPickerContext: { _ in }, completion: { [weak self] fromGallery, signals, silentPosting, scheduleTime, parameters, getAnimatedTransitionSource, completion in + self?.enqueueMediaMessages(fromGallery: fromGallery, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, parameters: parameters, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) }) } }, diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift index 798f9bf023..aa4889525c 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift @@ -236,7 +236,8 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no forwardMessageIds: selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], canMakePaidContent: false, currentPrice: nil, - hasTimers: false + hasTimers: false, + sendPaidMessageStars: selfController.presentationInterfaceState.sendPaidMessageStars )), hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 804d6f3568..430f87c869 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -132,6 +132,7 @@ import BrowserUI import NotificationPeerExceptionController import AdsReportScreen import AdUI +import ChatMessagePaymentAlertController public enum ChatControllerPeekActions { case standard @@ -627,6 +628,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var currentSendStarsUndoMessageId: EngineMessage.Id? var currentSendStarsUndoCount: Int = 0 + weak var currentPaidMessageUndoController: UndoOverlayController? + let initTimestamp: Double public var alwaysShowSearchResultsAsList: Bool = false { @@ -2128,89 +2131,94 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let _ = sourceView.asyncdisplaykit_node as? ChatEmptyNodeStickerContentNode { shouldAnimateMessageTransition = true } - - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.chatDisplayNode.collapseInput() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in - var current = current - current = current.updatedInterfaceState { interfaceState in - var interfaceState = interfaceState - interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) - if clearInput { - interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString())) - } - return interfaceState - }.updatedInputMode { current in - if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { - return .media(mode: mode, expanded: nil, focused: focused) - } - return current - } - - return current - }) - } - }, shouldAnimateMessageTransition ? correlationId : nil) - - if shouldAnimateMessageTransition { - if let sourceNode = sourceView.asyncdisplaykit_node as? ChatMediaInputStickerGridItemNode { - strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .inputPanel(itemNode: sourceNode), replyPanel: replyPanel), initiated: { - guard let strongSelf = self else { - return - } - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in - var current = current - current = current.updatedInputMode { current in - if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { - return .media(mode: mode, expanded: nil, focused: focused) - } - return current - } - - return current - }) - }) - } else if let sourceNode = sourceView.asyncdisplaykit_node as? HorizontalStickerGridItemNode { - strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .mediaPanel(itemNode: sourceNode), replyPanel: replyPanel), initiated: {}) - } else if let sourceNode = sourceView.asyncdisplaykit_node as? ChatEmptyNodeStickerContentNode { - strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .emptyPanel(itemNode: sourceNode), replyPanel: nil), initiated: {}) - } else if let sourceLayer = sourceLayer { - strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .universal(sourceContainerView: sourceView, sourceRect: sourceRect, sourceLayer: sourceLayer), replyPanel: replyPanel), initiated: { - guard let strongSelf = self else { - return - } - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in - var current = current - current = current.updatedInputMode { current in - if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { - return .media(mode: mode, expanded: nil, focused: focused) - } - return current - } - - return current - }) - }) - } - } - let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: fileReference.abstract, threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)] - if silentPosting { - let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting) - strongSelf.sendMessages(transformedMessages) - } else if schedule { - strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime in + strongSelf.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in + guard let strongSelf = self else { + return + } + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { - let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime) - strongSelf.sendMessages(transformedMessages) + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in + var current = current + current = current.updatedInterfaceState { interfaceState in + var interfaceState = interfaceState + interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) + if clearInput { + interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString())) + } + return interfaceState + }.updatedInputMode { current in + if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil, focused: focused) + } + return current + } + + return current + }) } - }) - } else { - let transformedMessages = strongSelf.transformEnqueueMessages(messages) - strongSelf.sendMessages(transformedMessages) - } + }, shouldAnimateMessageTransition ? correlationId : nil) + + if shouldAnimateMessageTransition { + if let sourceNode = sourceView.asyncdisplaykit_node as? ChatMediaInputStickerGridItemNode { + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .inputPanel(itemNode: sourceNode), replyPanel: replyPanel), initiated: { + guard let strongSelf = self else { + return + } + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in + var current = current + current = current.updatedInputMode { current in + if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil, focused: focused) + } + return current + } + + return current + }) + }) + } else if let sourceNode = sourceView.asyncdisplaykit_node as? HorizontalStickerGridItemNode { + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .mediaPanel(itemNode: sourceNode), replyPanel: replyPanel), initiated: {}) + } else if let sourceNode = sourceView.asyncdisplaykit_node as? ChatEmptyNodeStickerContentNode { + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .emptyPanel(itemNode: sourceNode), replyPanel: nil), initiated: {}) + } else if let sourceLayer = sourceLayer { + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .universal(sourceContainerView: sourceView, sourceRect: sourceRect, sourceLayer: sourceLayer), replyPanel: replyPanel), initiated: { + guard let strongSelf = self else { + return + } + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in + var current = current + current = current.updatedInputMode { current in + if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil, focused: focused) + } + return current + } + + return current + }) + }) + } + } + + let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: fileReference.abstract, threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)] + if silentPosting { + let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting, postpone: postpone) + strongSelf.sendMessages(transformedMessages) + } else if schedule { + strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime in + if let strongSelf = self { + let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime, postpone: postpone) + strongSelf.sendMessages(transformedMessages) + } + }) + } else { + let transformedMessages = strongSelf.transformEnqueueMessages(messages, postpone: postpone) + strongSelf.sendMessages(transformedMessages) + } + }) return true }, sendEmoji: { [weak self] text, attribute, immediately in if let strongSelf = self { @@ -4594,9 +4602,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let controller = chatMessageRemovePaymentAlertController( context: self.context, + presentationData: self.presentationData, updatedPresentationData: self.updatedPresentationData, peer: peer, amount: (revenue?.value ?? 0) > 0 ? revenue : nil, + navigationController: self.navigationController as? NavigationController, completion: { [weak self] refund in guard let self else { return @@ -5769,6 +5779,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: canReportIrrelevantLocation, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy, managingBot: managingBot) + + if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, peer.flags.contains(.isCreator) || peer.adminRights != nil { + + } else { + sendPaidMessageStars = cachedData.sendPaidMessageStars + } } var peers = SimpleDictionary() @@ -8327,6 +8343,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case .standard(.default) = self.mode { self.hasBrowserOrAppInFront.set(.single(false)) } + + if let _ = self.currentPaidMessageUndoController, let peerId = self.chatLocation.peerId { + self.context.engine.messages.forceSendPostponedPaidMessage(peerId: peerId) + } } func saveInterfaceState(includeScrollState: Bool = true) { @@ -9019,9 +9039,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - func transformEnqueueMessages(_ messages: [EnqueueMessage]) -> [EnqueueMessage] { + func transformEnqueueMessages(_ messages: [EnqueueMessage], postpone: Bool = false) -> [EnqueueMessage] { let silentPosting = self.presentationInterfaceState.interfaceState.silentPosting - return transformEnqueueMessages(messages, silentPosting: silentPosting) + return transformEnqueueMessages(messages, silentPosting: silentPosting, postpone: postpone) } @discardableResult func dismissAllUndoControllers() -> UndoOverlayController? { @@ -9184,7 +9204,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - func transformEnqueueMessages(_ messages: [EnqueueMessage], silentPosting: Bool, scheduleTime: Int32? = nil) -> [EnqueueMessage] { + func transformEnqueueMessages(_ messages: [EnqueueMessage], silentPosting: Bool, scheduleTime: Int32? = nil, postpone: Bool = false) -> [EnqueueMessage] { var defaultReplyMessageSubject: EngineMessageReplySubject? switch self.chatLocation { case .peer: @@ -9223,8 +9243,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return message.withUpdatedAttributes { attributes in var attributes = attributes - if self.presentationInterfaceState.acknowledgedPaidMessage, let sendPaidMessageStars = self.presentationInterfaceState.sendPaidMessageStars { - attributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars)) + if let sendPaidMessageStars = self.presentationInterfaceState.sendPaidMessageStars { + attributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: postpone)) } if silentPosting || scheduleTime != nil { @@ -9261,7 +9281,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return .single(false) } - func sendMessages(_ messages: [EnqueueMessage], media: Bool = false, commit: Bool = false) { + func sendMessages(_ messages: [EnqueueMessage], media: Bool = false, postpone: Bool = false, commit: Bool = false) { if case let .customChatContents(customChatContents) = self.subject { customChatContents.enqueueMessages(messages: messages) return @@ -9300,7 +9320,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if commit || !isScheduledMessages { self.commitPurposefulAction() - let _ = (enqueueMessages(account: self.context.account, peerId: peerId, messages: self.transformEnqueueMessages(messages)) + let _ = (enqueueMessages(account: self.context.account, peerId: peerId, messages: self.transformEnqueueMessages(messages, postpone: postpone)) |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in if let strongSelf = self, strongSelf.presentationInterfaceState.subject != .scheduledMessages { strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() @@ -9323,14 +9343,24 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { self.presentScheduleTimePicker(style: media ? .media : .default, dismissByTapOutside: false, completion: { [weak self] time in if let strongSelf = self { - strongSelf.sendMessages(strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: time), commit: true) + strongSelf.sendMessages(strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: time, postpone: postpone), commit: true) } }) } }) } - func enqueueMediaMessages(signals: [Any]?, silentPosting: Bool, scheduleTime: Int32? = nil, parameters: ChatSendMessageActionSheetController.SendParameters? = nil, getAnimatedTransitionSource: ((String) -> UIView?)? = nil, completion: @escaping () -> Void = {}) { + func enqueueMediaMessages(fromGallery: Bool = false, signals: [Any]?, silentPosting: Bool, scheduleTime: Int32? = nil, parameters: ChatSendMessageActionSheetController.SendParameters? = nil, getAnimatedTransitionSource: ((String) -> UIView?)? = nil, completion: @escaping () -> Void = {}) { + if let _ = self.presentationInterfaceState.sendPaidMessageStars { + self.presentPaidMessageAlertIfNeeded(count: Int32(signals?.count ?? 1), forceDark: fromGallery, completion: { [weak self] postpone in + self?.commitEnqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, postpone: postpone, parameters: parameters, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) + }) + } else { + self.commitEnqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, parameters: parameters, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) + } + } + + private func commitEnqueueMediaMessages(signals: [Any]?, silentPosting: Bool, scheduleTime: Int32? = nil, postpone: Bool = false, parameters: ChatSendMessageActionSheetController.SendParameters? = nil, getAnimatedTransitionSource: ((String) -> UIView?)? = nil, completion: @escaping () -> Void = {}) { self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(context: self.context, account: self.context.account, signals: signals!) |> deliverOnMainQueue).startStrict(next: { [weak self] items in guard let strongSelf = self else { @@ -9463,7 +9493,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - let messages = strongSelf.transformEnqueueMessages(mappedMessages, silentPosting: silentPosting, scheduleTime: scheduleTime) + let messages = strongSelf.transformEnqueueMessages(mappedMessages, silentPosting: silentPosting, scheduleTime: scheduleTime, postpone: postpone) let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { @@ -9485,7 +9515,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } - func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, resetTextInputState: Bool = true) { + func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, resetTextInputState: Bool = true, postpone: Bool = false) { if !canSendMessagesToChat(self.presentationInterfaceState) { return } @@ -9504,7 +9534,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } let replyMessageSubject = self.presentationInterfaceState.interfaceState.replyMessageSubject - if self.context.engine.messages.enqueueOutgoingMessageWithChatContextResult(to: peerId, threadId: self.chatLocation.threadId, botId: results.botId, result: result, replyToMessageId: replyMessageSubject?.subjectModel, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime) { + + let sendPaidMessageStars = self.presentationInterfaceState.sendPaidMessageStars + if self.context.engine.messages.enqueueOutgoingMessageWithChatContextResult(to: peerId, threadId: self.chatLocation.threadId, botId: results.botId, result: result, replyToMessageId: replyMessageSubject?.subjectModel, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, sendPaidMessageStars: sendPaidMessageStars, postpone: postpone) { self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() diff --git a/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift b/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift index 401c692bec..e36b0510f5 100644 --- a/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift +++ b/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift @@ -13,8 +13,9 @@ import ChatInterfaceState import PremiumUI import ReactionSelectionNode import TopMessageReactions +import ChatMessagePaymentAlertController -extension ChatControllerImpl { +extension ChatControllerImpl { func forwardMessages(messageIds: [MessageId], options: ChatInterfaceForwardOptionsState? = nil, resetCurrent: Bool = false) { let _ = (self.context.engine.data.get(EngineDataMap( messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init) @@ -94,190 +95,246 @@ extension ChatControllerImpl { } } controller.multiplePeersSelected = { [weak self, weak controller] peers, peerMap, messageText, mode, forwardOptions, _ in - guard let strongSelf = self, let strongController = controller else { - return - } - strongController.dismiss() + let peerIds = peers.map { $0.id } - var result: [EnqueueMessage] = [] - if messageText.string.count > 0 { - let inputText = convertMarkdownToAttributes(messageText) - for text in breakChatInputText(trimChatInputText(inputText)) { - if text.length != 0 { - var attributes: [MessageAttribute] = [] - let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) - if !entities.isEmpty { - attributes.append(TextEntitiesMessageAttribute(entities: entities)) - } - result.append(.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) - } - } - } - - var attributes: [MessageAttribute] = [] - attributes.append(ForwardOptionsMessageAttribute(hideNames: forwardOptions?.hideNames == true, hideCaptions: forwardOptions?.hideCaptions == true)) - - result.append(contentsOf: messages.map { message -> EnqueueMessage in - return .forward(source: message.id, threadId: nil, grouping: .auto, attributes: attributes, correlationId: nil) - }) - - let commit: ([EnqueueMessage]) -> Void = { result in + let _ = (context.engine.data.get( + EngineDataMap( + peerIds.map(TelegramEngine.EngineData.Item.Peer.SendPaidMessageStars.init(id:)) + ) + ) + |> deliverOnMainQueue).start(next: { [weak self, weak controller] sendPaidMessageStars in guard let strongSelf = self else { return } - var result = result - - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }).updatedSearch(nil) }) - - var correlationIds: [Int64] = [] - for i in 0 ..< result.count { - let correlationId = Int64.random(in: Int64.min ... Int64.max) - correlationIds.append(correlationId) - result[i] = result[i].withUpdatedCorrelationId(correlationId) + var count: Int32 = Int32(messages.count) + if messageText.string.count > 0 { + count += 1 } - - let targetPeersShouldDivertSignals: [Signal<(EnginePeer, Bool), NoError>] = peers.map { peer -> Signal<(EnginePeer, Bool), NoError> in - return strongSelf.shouldDivertMessagesToScheduled(targetPeer: peer, messages: result) - |> map { shouldDivert -> (EnginePeer, Bool) in - return (peer, shouldDivert) + var totalAmount: StarsAmount = .zero + for peer in peers { + if let maybeAmount = sendPaidMessageStars[peer.id], let amount = maybeAmount { + totalAmount = totalAmount + amount } } - let targetPeersShouldDivert: Signal<[(EnginePeer, Bool)], NoError> = combineLatest(targetPeersShouldDivertSignals) - let _ = (targetPeersShouldDivert - |> deliverOnMainQueue).startStandalone(next: { targetPeersShouldDivert in - guard let strongSelf = self else { + + let proceed = { [weak self, weak controller] in + guard let strongSelf = self, let strongController = controller else { return } - var displayConvertingTooltip = false + strongController.dismiss() - var displayPeers: [EnginePeer] = [] - for (peer, shouldDivert) in targetPeersShouldDivert { - var peerMessages = result - if shouldDivert { - displayConvertingTooltip = true - peerMessages = peerMessages.map { message -> EnqueueMessage in - return message.withUpdatedAttributes { attributes in - var attributes = attributes - attributes.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute }) - attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: Int32(Date().timeIntervalSince1970) + 10 * 24 * 60 * 60)) - return attributes + var result: [EnqueueMessage] = [] + if messageText.string.count > 0 { + let inputText = convertMarkdownToAttributes(messageText) + for text in breakChatInputText(trimChatInputText(inputText)) { + if text.length != 0 { + var attributes: [MessageAttribute] = [] + let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) + if !entities.isEmpty { + attributes.append(TextEntitiesMessageAttribute(entities: entities)) } + result.append(.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } } - - let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: peerMessages) - |> deliverOnMainQueue).startStandalone(next: { messageIds in - if let strongSelf = self { - let signals: [Signal] = messageIds.compactMap({ id -> Signal? in - guard let id = id else { - return nil - } - return strongSelf.context.account.pendingMessageManager.pendingMessageStatus(id) - |> mapToSignal { status, _ -> Signal in - if status != nil { - return .never() - } else { - return .single(true) - } - } - |> take(1) - }) - if strongSelf.shareStatusDisposable == nil { - strongSelf.shareStatusDisposable = MetaDisposable() - } - strongSelf.shareStatusDisposable?.set((combineLatest(signals) - |> deliverOnMainQueue).startStrict()) - } - }) - - if case let .secretChat(secretPeer) = peer { - if let peer = peerMap[secretPeer.regularPeerId] { - displayPeers.append(peer) - } - } else { - displayPeers.append(peer) - } - } - - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let text: String - var savedMessages = false - if displayPeers.count == 1, let peerId = displayPeers.first?.id, peerId == strongSelf.context.account.peerId { - text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many - savedMessages = true - } else { - if displayPeers.count == 1, let peer = displayPeers.first { - var peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string : presentationData.strings.Conversation_ForwardTooltip_Chat_Many(peerName).string - } else if displayPeers.count == 2, let firstPeer = displayPeers.first, let secondPeer = displayPeers.last { - var firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") - var secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") - text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string : presentationData.strings.Conversation_ForwardTooltip_TwoChats_Many(firstPeerName, secondPeerName).string - } else if let peer = displayPeers.first { - var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(displayPeers.count - 1)").string : presentationData.strings.Conversation_ForwardTooltip_ManyChats_Many(peerName, "\(displayPeers.count - 1)").string - } else { - text = "" - } } - let reactionItems: Signal<[ReactionItem], NoError> - if savedMessages && messages.count > 0 { - reactionItems = tagMessageReactions(context: strongSelf.context, subPeerId: nil) - } else { - reactionItems = .single([]) - } + var attributes: [MessageAttribute] = [] + attributes.append(ForwardOptionsMessageAttribute(hideNames: forwardOptions?.hideNames == true, hideCaptions: forwardOptions?.hideCaptions == true)) - let _ = (reactionItems - |> deliverOnMainQueue).startStandalone(next: { [weak strongSelf] reactionItems in - guard let strongSelf else { - return - } - - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, position: savedMessages && messages.count > 0 ? .top : .bottom, animateInAsReplacement: true, action: { action in - if savedMessages, let self, action == .info { - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self, let peer else { - return - } - guard let navigationController = self.navigationController as? NavigationController else { - return - } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) - }) - } - return false - }, additionalView: (savedMessages && messages.count > 0) ? chatShareToSavedMessagesAdditionalView(strongSelf, reactionItems: reactionItems, correlationIds: correlationIds) : nil), in: .current) + result.append(contentsOf: messages.map { message -> EnqueueMessage in + return .forward(source: message.id, threadId: nil, grouping: .auto, attributes: attributes, correlationId: nil) }) - if displayConvertingTooltip { + let commit: ([EnqueueMessage]) -> Void = { result in + guard let strongSelf = self else { + return + } + var result = result + + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }).updatedSearch(nil) }) + + var correlationIds: [Int64] = [] + for i in 0 ..< result.count { + let correlationId = Int64.random(in: Int64.min ... Int64.max) + correlationIds.append(correlationId) + result[i] = result[i].withUpdatedCorrelationId(correlationId) + } + + let targetPeersShouldDivertSignals: [Signal<(EnginePeer, Bool), NoError>] = peers.map { peer -> Signal<(EnginePeer, Bool), NoError> in + return strongSelf.shouldDivertMessagesToScheduled(targetPeer: peer, messages: result) + |> map { shouldDivert -> (EnginePeer, Bool) in + return (peer, shouldDivert) + } + } + let targetPeersShouldDivert: Signal<[(EnginePeer, Bool)], NoError> = combineLatest(targetPeersShouldDivertSignals) + let _ = (targetPeersShouldDivert + |> deliverOnMainQueue).startStandalone(next: { targetPeersShouldDivert in + guard let strongSelf = self else { + return + } + + var displayConvertingTooltip = false + + var displayPeers: [EnginePeer] = [] + for (peer, shouldDivert) in targetPeersShouldDivert { + var peerMessages = result + if shouldDivert { + displayConvertingTooltip = true + peerMessages = peerMessages.map { message -> EnqueueMessage in + return message.withUpdatedAttributes { attributes in + var attributes = attributes + attributes.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute }) + attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: Int32(Date().timeIntervalSince1970) + 10 * 24 * 60 * 60)) + return attributes + } + } + } + + if let maybeAmount = sendPaidMessageStars[peer.id], let amount = maybeAmount { + peerMessages = peerMessages.map { message -> EnqueueMessage in + return message.withUpdatedAttributes { attributes in + var attributes = attributes + attributes.append(PaidStarsMessageAttribute(stars: amount, postponeSending: false)) + return attributes + } + } + } + + let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: peerMessages) + |> deliverOnMainQueue).startStandalone(next: { messageIds in + if let strongSelf = self { + let signals: [Signal] = messageIds.compactMap({ id -> Signal? in + guard let id = id else { + return nil + } + return strongSelf.context.account.pendingMessageManager.pendingMessageStatus(id) + |> mapToSignal { status, _ -> Signal in + if status != nil { + return .never() + } else { + return .single(true) + } + } + |> take(1) + }) + if strongSelf.shareStatusDisposable == nil { + strongSelf.shareStatusDisposable = MetaDisposable() + } + strongSelf.shareStatusDisposable?.set((combineLatest(signals) + |> deliverOnMainQueue).startStrict()) + } + }) + + if case let .secretChat(secretPeer) = peer { + if let peer = peerMap[secretPeer.regularPeerId] { + displayPeers.append(peer) + } + } else { + displayPeers.append(peer) + } + } + + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let text: String + var savedMessages = false + if displayPeers.count == 1, let peerId = displayPeers.first?.id, peerId == strongSelf.context.account.peerId { + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many + savedMessages = true + } else { + if displayPeers.count == 1, let peer = displayPeers.first { + var peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string : presentationData.strings.Conversation_ForwardTooltip_Chat_Many(peerName).string + } else if displayPeers.count == 2, let firstPeer = displayPeers.first, let secondPeer = displayPeers.last { + var firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") + var secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string : presentationData.strings.Conversation_ForwardTooltip_TwoChats_Many(firstPeerName, secondPeerName).string + } else if let peer = displayPeers.first { + var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(displayPeers.count - 1)").string : presentationData.strings.Conversation_ForwardTooltip_ManyChats_Many(peerName, "\(displayPeers.count - 1)").string + } else { + text = "" + } + } + + let reactionItems: Signal<[ReactionItem], NoError> + if savedMessages && messages.count > 0 { + reactionItems = tagMessageReactions(context: strongSelf.context, subPeerId: nil) + } else { + reactionItems = .single([]) + } + + let _ = (reactionItems + |> deliverOnMainQueue).startStandalone(next: { [weak strongSelf] reactionItems in + guard let strongSelf else { + return + } + + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, position: savedMessages && messages.count > 0 ? .top : .bottom, animateInAsReplacement: true, action: { action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) + }) + } + return false + }, additionalView: (savedMessages && messages.count > 0) ? chatShareToSavedMessagesAdditionalView(strongSelf, reactionItems: reactionItems, correlationIds: correlationIds) : nil), in: .current) + }) + + if displayConvertingTooltip { + } + }) } - }) - } - - switch mode { - case .generic: - commit(result) - case .silent: - let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: true) - commit(transformedMessages) - case .schedule: - strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime in - if let strongSelf = self { - let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: false, scheduleTime: scheduleTime) + + switch mode { + case .generic: + commit(result) + case .silent: + let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: true) + commit(transformedMessages) + case .schedule: + strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime in + if let strongSelf = self { + let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: false, scheduleTime: scheduleTime) + commit(transformedMessages) + } + }) + case .whenOnline: + let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: false, scheduleTime: scheduleWhenOnlineTimestamp) commit(transformedMessages) } - }) - case .whenOnline: - let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: false, scheduleTime: scheduleWhenOnlineTimestamp) - commit(transformedMessages) - } + } + + if totalAmount.value > 0 { + let controller = chatMessagePaymentAlertController( + context: nil, + presentationData: strongSelf.presentationData, + updatedPresentationData: nil, + peers: peers, + count: count, + amount: totalAmount, + totalAmount: totalAmount, + hasCheck: false, + navigationController: strongSelf.navigationController as? NavigationController, + completion: { _ in + proceed() + } + ) + strongSelf.present(controller, in: .window(.root)) + } else { + proceed() + } + }) } controller.peerSelected = { [weak self, weak controller] peer, threadId in guard let strongSelf = self, let strongController = controller else { diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 466e2412d0..043144bae6 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -273,7 +273,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { var requestUpdateChatInterfaceState: (ContainedViewLayoutTransition, Bool, (ChatInterfaceState) -> ChatInterfaceState) -> Void = { _, _, _ in } var requestUpdateInterfaceState: (ContainedViewLayoutTransition, Bool, (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void = { _, _, _ in } - var sendMessages: ([EnqueueMessage], Bool?, Int32?, Bool) -> Void = { _, _, _, _ in } + var sendMessages: ([EnqueueMessage], Bool?, Int32?, Bool, Bool) -> Void = { _, _, _, _, _ in } var displayAttachmentMenu: () -> Void = { } var paste: (ChatTextInputPanelPasteData) -> Void = { _ in } var updateTypingActivity: (Bool) -> Void = { _ in } @@ -881,11 +881,24 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } self.textInputPanelNode?.sendMessage = { [weak self] in - if let strongSelf = self { - if case .scheduledMessages = strongSelf.chatPresentationInterfaceState.subject, strongSelf.chatPresentationInterfaceState.editMessageState == nil { - strongSelf.controllerInteraction.scheduleCurrentMessage(nil) + if let self, let controller = self.controller { + if case .scheduledMessages = self.chatPresentationInterfaceState.subject, self.chatPresentationInterfaceState.editMessageState == nil { + self.controllerInteraction.scheduleCurrentMessage(nil) } else { - strongSelf.sendCurrentMessage() + if let _ = self.chatPresentationInterfaceState.sendPaidMessageStars { + var count: Int32 = 1 + if let forwardedCount = self.chatPresentationInterfaceState.interfaceState.forwardMessageIds?.count, forwardedCount > 0 { + count = Int32(forwardedCount) + if self.chatPresentationInterfaceState.interfaceState.effectiveInputState.inputText.length > 0 { + count += 1 + } + } + controller.presentPaidMessageAlertIfNeeded(count: count, completion: { [weak self] postpone in + self?.sendCurrentMessage(postpone: postpone) + }) + } else { + self.sendCurrentMessage() + } } } } @@ -4057,7 +4070,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } - func sendCurrentMessage(silentPosting: Bool? = nil, scheduleTime: Int32? = nil, messageEffect: ChatSendMessageEffect? = nil, completion: @escaping () -> Void = {}) { + func sendCurrentMessage(silentPosting: Bool? = nil, scheduleTime: Int32? = nil, postpone: Bool = false, messageEffect: ChatSendMessageEffect? = nil, completion: @escaping () -> Void = {}) { if let textInputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { self.historyNode.justSentTextMessage = true @@ -4323,7 +4336,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { }, usedCorrelationId) completion() - self.sendMessages(messages, silentPosting, scheduleTime, messages.count > 1) + self.sendMessages(messages, silentPosting, scheduleTime, messages.count > 1, postpone) } } } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index 58afd63dd1..770371f44d 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -118,6 +118,11 @@ extension ChatControllerImpl { isScheduledMessages = true } + var isPaidMessages = false + if let _ = self.presentationInterfaceState.sendPaidMessageStars { + isPaidMessages = true + } + var peerType: AttachMenuBots.Bot.PeerFlags = [] if let peer = self.presentationInterfaceState.renderedPeer?.peer { if let user = peer as? TelegramUser { @@ -136,7 +141,7 @@ extension ChatControllerImpl { } } } - + let buttons: Signal<([AttachmentButtonType], [AttachmentButtonType], AttachmentButtonType?), NoError> if let peer = self.presentationInterfaceState.renderedPeer?.peer, !isScheduledMessages, !peer.isDeleted { buttons = combineLatest( @@ -158,35 +163,37 @@ extension ChatControllerImpl { break } - for bot in attachMenuBots.reversed() { - var peerType = peerType - if bot.peer.id == peer.id { - peerType.insert(.sameBot) - peerType.remove(.bot) - } - let button: AttachmentButtonType = .app(bot) - if !bot.peerTypes.intersection(peerType).isEmpty { - buttons.insert(button, at: 1) - - if case let .bot(botId, _, _) = subject { - if initialButton == nil && bot.peer.id == botId { - initialButton = button + if !isPaidMessages { + for bot in attachMenuBots.reversed() { + var peerType = peerType + if bot.peer.id == peer.id { + peerType.insert(.sameBot) + peerType.remove(.bot) + } + let button: AttachmentButtonType = .app(bot) + if !bot.peerTypes.intersection(peerType).isEmpty { + buttons.insert(button, at: 1) + + if case let .bot(botId, _, _) = subject { + if initialButton == nil && bot.peer.id == botId { + initialButton = button + } } } + allButtons.insert(button, at: 1) } - allButtons.insert(button, at: 1) - } - if let user = peer as? TelegramUser, user.botInfo == nil { - if let index = buttons.firstIndex(where: { $0 == .location }) { - buttons.insert(.quickReply, at: index + 1) - } else { - buttons.append(.quickReply) - } - if let index = allButtons.firstIndex(where: { $0 == .location }) { - allButtons.insert(.quickReply, at: index + 1) - } else { - allButtons.append(.quickReply) + if let user = peer as? TelegramUser, user.botInfo == nil { + if let index = buttons.firstIndex(where: { $0 == .location }) { + buttons.insert(.quickReply, at: index + 1) + } else { + buttons.append(.quickReply) + } + if let index = allButtons.firstIndex(where: { $0 == .location }) { + allButtons.insert(.quickReply, at: index + 1) + } else { + allButtons.append(.quickReply) + } } } @@ -320,11 +327,11 @@ extension ChatControllerImpl { completion(controller, mediaPickerContext) }, updateMediaPickerContext: { [weak attachmentController] mediaPickerContext in attachmentController?.mediaPickerContext = mediaPickerContext - }, completion: { [weak self] signals, silentPosting, scheduleTime, parameters, getAnimatedTransitionSource, completion in + }, completion: { [weak self] fromGallery, signals, silentPosting, scheduleTime, parameters, getAnimatedTransitionSource, completion in if !inputText.string.isEmpty { self?.clearInputText() } - self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, parameters: parameters, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) + self?.enqueueMediaMessages(fromGallery: fromGallery, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, parameters: parameters, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) }) case .file: strongSelf.controllerNavigationDisposable.set(nil) @@ -341,12 +348,13 @@ extension ChatControllerImpl { attachmentController?.dismiss(animated: true) self?.presentICloudFileGallery() }, send: { [weak self] mediaReference in - guard let strongSelf = self else { + guard let self else { return } - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - strongSelf.sendMessages([message], media: true) + self.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in + self?.sendMessages([message], media: true, postpone: postpone) + }) }) if let controller = controller as? AttachmentFileControllerImpl { let _ = currentFilesController.swap(controller) @@ -372,7 +380,7 @@ extension ChatControllerImpl { } else { selfPeerId = strongSelf.context.account.peerId } - let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: selfPeerId)) + ;let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: selfPeerId)) |> deliverOnMainQueue).startStandalone(next: { selfPeer in guard let strongSelf = self, let selfPeer = selfPeer else { return @@ -390,16 +398,22 @@ extension ChatControllerImpl { } let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.chatDisplayNode.collapseInput() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } - }) + + strongSelf.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in + guard let strongSelf = self else { + return } - }, nil) - strongSelf.sendMessages([message]) + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } + }) + } + }, nil) + strongSelf.sendMessages([message], postpone: postpone) + }) }) completion(controller, controller.mediaPickerContext) @@ -478,7 +492,12 @@ extension ChatControllerImpl { return attributes } } - strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) + strongSelf.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in + guard let strongSelf = self else { + return + } + strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) + }) } else if let peer = peers.first { let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError> switch peer { @@ -547,7 +566,12 @@ extension ChatControllerImpl { } } enqueueMessages.append(.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) - strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) + strongSelf.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in + guard let strongSelf = self else { + return + } + strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime), postpone: postpone) + }) } else { let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: strongSelf.context), environment: ShareControllerAppEnvironment(sharedContext: strongSelf.context.sharedContext), subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else { @@ -572,7 +596,12 @@ extension ChatControllerImpl { enqueueMessages.append(textEnqueueMessage) } enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) - strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) + strongSelf.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in + guard let strongSelf = self else { + return + } + strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime), postpone: postpone) + }) } }), completed: nil, cancelled: nil) strongSelf.effectiveNavigationController?.pushViewController(contactController) @@ -811,7 +840,7 @@ extension ChatControllerImpl { if let channel = peer as? TelegramChannel, channel.isRestrictedBySlowmode { slowModeEnabled = true } - hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat + hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat && strongSelf.presentationInterfaceState.sendPaidMessageStars == nil } let controller = legacyAttachmentMenu( @@ -877,7 +906,7 @@ extension ChatControllerImpl { if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { storeCapturedPhotos = peer.id.namespace != Namespaces.Peer.SecretChat - hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat + hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat && strongSelf.presentationInterfaceState.sendPaidMessageStars == nil } presentedLegacyCamera(context: strongSelf.context, peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: storeCapturedPhotos, mediaGrouping: true, initialCaption: inputText, hasSchedule: hasSchedule, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, _, _, _ in @@ -1125,7 +1154,12 @@ extension ChatControllerImpl { }) } }, nil) - strongSelf.sendMessages(messages) + strongSelf.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in + guard let strongSelf = self else { + return + } + strongSelf.sendMessages(messages, postpone: postpone) + }) } } } @@ -1159,7 +1193,7 @@ extension ChatControllerImpl { self.present(actionSheet, in: .window(.root)) } - func presentMediaPicker(subject: MediaPickerScreenImpl.Subject = .assets(nil, .default), saveEditedPhotos: Bool, bannedSendPhotos: (Int32, Bool)?, bannedSendVideos: (Int32, Bool)?, present: @escaping (MediaPickerScreenImpl, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping ([Any], Bool, Int32?, ChatSendMessageActionSheetController.SendParameters?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void) { + func presentMediaPicker(subject: MediaPickerScreenImpl.Subject = .assets(nil, .default), saveEditedPhotos: Bool, bannedSendPhotos: (Int32, Bool)?, bannedSendVideos: (Int32, Bool)?, present: @escaping (MediaPickerScreenImpl, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping (Bool, [Any], Bool, Int32?, ChatSendMessageActionSheetController.SendParameters?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void) { var isScheduledMessages = false if case .scheduledMessages = self.presentationInterfaceState.subject { isScheduledMessages = true @@ -1180,6 +1214,7 @@ extension ChatControllerImpl { canBoostToUnrestrict: (self.presentationInterfaceState.boostsToUnrestrict ?? 0) > 0 && bannedSendPhotos?.1 != true && bannedSendVideos?.1 != true, paidMediaAllowed: paidMediaAllowed, subject: subject, + sendPaidMessageStars: self.presentationInterfaceState.sendPaidMessageStars?.value, saveEditedPhotos: saveEditedPhotos ) controller.openBoost = { [weak self, weak controller] in @@ -1238,8 +1273,8 @@ extension ChatControllerImpl { controller.getCaptionPanelView = { [weak self] in return self?.getCaptionPanelView(isFile: false) } - controller.legacyCompletion = { signals, silently, scheduleTime, parameters, getAnimatedTransitionSource, sendCompletion in - completion(signals, silently, scheduleTime, parameters, getAnimatedTransitionSource, sendCompletion) + controller.legacyCompletion = { fromGallery, signals, silently, scheduleTime, parameters, getAnimatedTransitionSource, sendCompletion in + completion(fromGallery, signals, silently, scheduleTime, parameters, getAnimatedTransitionSource, sendCompletion) } controller.editCover = { [weak self] dimensions, completion in guard let self else { @@ -1559,16 +1594,21 @@ extension ChatControllerImpl { } let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.chatDisplayNode.collapseInput() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } - }) + strongSelf.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in + guard let strongSelf = self else { + return } - }, nil) - strongSelf.sendMessages([message]) + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } + }) + } + }, nil) + strongSelf.sendMessages([message], postpone: postpone) + }) }) strongSelf.effectiveNavigationController?.pushViewController(controller) strongSelf.chatDisplayNode.dismissInput() @@ -1621,7 +1661,12 @@ extension ChatControllerImpl { enqueueMessages.append(message) } } - strongSelf.sendMessages(enqueueMessages) + strongSelf.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in + guard let strongSelf = self else { + return + } + strongSelf.sendMessages(enqueueMessages, postpone: postpone) + }) } else if let peer = peers.first { let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError> switch peer { @@ -1679,7 +1724,12 @@ extension ChatControllerImpl { } }, nil) let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - strongSelf.sendMessages([message]) + strongSelf.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in + guard let strongSelf = self else { + return + } + strongSelf.sendMessages([message], postpone: postpone) + }) } else { let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: strongSelf.context), environment: ShareControllerAppEnvironment(sharedContext: strongSelf.context.sharedContext), subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else { @@ -1699,7 +1749,12 @@ extension ChatControllerImpl { } }, nil) let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - strongSelf.sendMessages([message]) + strongSelf.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in + guard let strongSelf = self else { + return + } + strongSelf.sendMessages([message], postpone: postpone) + }) } }), completed: nil, cancelled: nil) strongSelf.effectiveNavigationController?.pushViewController(contactController) @@ -1775,7 +1830,7 @@ extension ChatControllerImpl { var hasSchedule = false if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { storeCapturedMedia = peer.id.namespace != Namespaces.Peer.SecretChat - hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat + hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat && strongSelf.presentationInterfaceState.sendPaidMessageStars == nil } let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 8bf597a088..a9678b1602 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -420,19 +420,28 @@ func chatHistoryEntriesForView( if includeChatInfoEntry { if view.earlierId == nil, !view.isLoading { + var chatPeer: Peer? var cachedPeerData: CachedPeerData? for entry in view.additionalData { if case let .cachedPeerData(_, data) = entry { cachedPeerData = data - break + } else if case let .peer(_, peer) = entry { + chatPeer = peer } } if case let .peer(peerId) = location, peerId.isReplies { - entries.insert(.ChatInfoEntry("", presentationData.strings.RepliesChat_DescriptionText, nil, nil, presentationData), at: 0) + entries.insert(.ChatInfoEntry(.botInfo(title: "", text: presentationData.strings.RepliesChat_DescriptionText, photo: nil, video: nil), presentationData), at: 0) } else if case let .peer(peerId) = location, peerId.isVerificationCodes { - entries.insert(.ChatInfoEntry("", presentationData.strings.VerificationCodes_DescriptionText, nil, nil, presentationData), at: 0) - } else if let cachedPeerData = cachedPeerData as? CachedUserData, let botInfo = cachedPeerData.botInfo, !botInfo.description.isEmpty { - entries.insert(.ChatInfoEntry(presentationData.strings.Bot_DescriptionTitle, botInfo.description, botInfo.photo, botInfo.video, presentationData), at: 0) + entries.insert(.ChatInfoEntry(.botInfo(title: "", text: presentationData.strings.VerificationCodes_DescriptionText, photo: nil, video: nil), presentationData), at: 0) + } else if let cachedPeerData = cachedPeerData as? CachedUserData { + if let botInfo = cachedPeerData.botInfo, !botInfo.description.isEmpty { + entries.insert(.ChatInfoEntry(.botInfo(title: presentationData.strings.Bot_DescriptionTitle, text: botInfo.description, photo: botInfo.photo, video: botInfo.video), presentationData), at: 0) + } else if let peerStatusSettings = cachedPeerData.peerStatusSettings, peerStatusSettings.registrationDate != nil || peerStatusSettings.phoneCountry != nil || peerStatusSettings.locationCountry != nil { + if peerStatusSettings.flags.contains(.canAddContact) || peerStatusSettings.flags.contains(.canReport) || peerStatusSettings.flags.contains(.canBlock) { + let title = chatPeer.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) ?? "" + entries.insert(.ChatInfoEntry(.userInfo(title: title, registrationDate: peerStatusSettings.registrationDate, phoneCountry: peerStatusSettings.phoneCountry, locationCountry: peerStatusSettings.locationCountry, groupsInCommon: []), presentationData), at: 0) + } + } } else { var isEmpty = true if entries.count <= 3 { diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index d101ace5e2..5ba8aac1f9 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -27,6 +27,7 @@ import TranslateUI import ChatHistoryEntry import ChatOverscrollControl import ChatBotInfoItem +import ChatUserInfoItem import ChatMessageItem import ChatMessageItemImpl import ChatMessageItemView @@ -254,8 +255,15 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(index: entry.entry.index, presentationData: presentationData, controllerInteraction: controllerInteraction, context: context), directionHint: entry.directionHint) case let .ReplyCountEntry(_, isComments, count, presentationData): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatReplyCountItem(index: entry.entry.index, isComments: isComments, count: count, presentationData: presentationData, context: context, controllerInteraction: controllerInteraction), directionHint: entry.directionHint) - case let .ChatInfoEntry(title, text, photo, video, presentationData): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatBotInfoItem(title: title, text: text, photo: photo, video: video, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context), directionHint: entry.directionHint) + case let .ChatInfoEntry(data, presentationData): + let item: ListViewItem + switch data { + case let .botInfo(title, text, photo, video): + item = ChatBotInfoItem(title: title, text: text, photo: photo, video: video, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context) + case let .userInfo(title, registrationDate, phoneCountry, locationCountry, groupsInCommon): + item = ChatUserInfoItem(title: title, registrationDate: registrationDate, phoneCountry: phoneCountry, locationCountry: locationCountry, groupsInCommon: groupsInCommon, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context) + } + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .SearchEntry(theme, strings): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: strings.Common_Search, activate: { controllerInteraction.openSearch() @@ -304,8 +312,15 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(index: entry.entry.index, presentationData: presentationData, controllerInteraction: controllerInteraction, context: context), directionHint: entry.directionHint) case let .ReplyCountEntry(_, isComments, count, presentationData): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatReplyCountItem(index: entry.entry.index, isComments: isComments, count: count, presentationData: presentationData, context: context, controllerInteraction: controllerInteraction), directionHint: entry.directionHint) - case let .ChatInfoEntry(title, text, photo, video, presentationData): - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatBotInfoItem(title: title, text: text, photo: photo, video: video, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context), directionHint: entry.directionHint) + case let .ChatInfoEntry(data, presentationData): + let item: ListViewItem + switch data { + case let .botInfo(title, text, photo, video): + item = ChatBotInfoItem(title: title, text: text, photo: photo, video: video, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context) + case let .userInfo(title, registrationDate, phoneCountry, locationCountry, groupsInCommon): + item = ChatUserInfoItem(title: title, registrationDate: registrationDate, phoneCountry: phoneCountry, locationCountry: locationCountry, groupsInCommon: groupsInCommon, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context) + } + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .SearchEntry(theme, strings): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: strings.Common_Search, activate: { controllerInteraction.openSearch() @@ -1346,9 +1361,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if peerId.namespace == Namespaces.Peer.CloudChannel { additionalData.append(.cacheEntry(cachedChannelAdminRanksEntryId(peerId: peerId))) } - if [Namespaces.Peer.CloudChannel, Namespaces.Peer.CloudGroup].contains(peerId.namespace) { - additionalData.append(.peer(peerId)) - } + additionalData.append(.peer(peerId)) if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat { additionalData.append(.peerIsContact(peerId)) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift index 292b53b3d9..ffc3b00db5 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift @@ -185,7 +185,7 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte showPremiumGift = true } } - if isTextEmpty, showPremiumGift, let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, !peer.isDeleted && peer.botInfo == nil && !peer.flags.contains(.isSupport) && !peer.isPremium && !chatPresentationInterfaceState.premiumGiftOptions.isEmpty && chatPresentationInterfaceState.suggestPremiumGift { + if isTextEmpty, showPremiumGift, let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, !peer.isDeleted && peer.botInfo == nil && !peer.flags.contains(.isSupport) && chatPresentationInterfaceState.suggestPremiumGift { accessoryItems.append(.gift) } } diff --git a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift index a01348b9ce..b13be5a2ea 100644 --- a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift @@ -19,6 +19,7 @@ import TooltipUI import TelegramNotices import ComponentFlow import MediaScrubberComponent +import AnimatedCountLabelNode //Xcode 16 #if canImport(ContactProvider) @@ -69,6 +70,10 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { let deleteButton: HighlightableButtonNode let binNode: AnimationNode let sendButton: HighlightTrackingButtonNode + let sendBackgroundNode: ASDisplayNode + let sendIconNode: ASImageNode + let textNode: ImmediateAnimatedCountLabelNode + private var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode? let playButton: HighlightableButtonNode private let playPauseIconNode: PlayPauseIconNode @@ -114,7 +119,16 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { self.sendButton = HighlightTrackingButtonNode() self.sendButton.displaysAsynchronously = false self.sendButton.isExclusiveTouch = true - self.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(theme), for: []) + + self.sendBackgroundNode = ASDisplayNode() + self.sendBackgroundNode.backgroundColor = theme.chat.inputPanel.actionControlFillColor + + self.sendIconNode = ASImageNode() + self.sendIconNode.displaysAsynchronously = false + self.sendIconNode.image = PresentationResourcesChat.chatInputPanelSendIconImage(theme) + + self.textNode = ImmediateAnimatedCountLabelNode() + self.textNode.isUserInteractionEnabled = false self.viewOnceButton = ChatRecordingViewOnceButtonNode(icon: .viewOnce) self.recordMoreButton = ChatRecordingViewOnceButtonNode(icon: .recordMore) @@ -167,6 +181,9 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { self.deleteButton.addSubnode(self.binNode) self.addSubnode(self.waveformBackgroundNode) self.addSubnode(self.sendButton) + self.sendButton.addSubnode(self.sendBackgroundNode) + self.sendButton.addSubnode(self.sendIconNode) + self.sendButton.addSubnode(self.textNode) self.addSubnode(self.waveformScrubberNode) self.addSubnode(self.playButton) self.addSubnode(self.durationLabel) @@ -249,6 +266,55 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { if self.presentationInterfaceState == nil { isFirstTime = true } + + var innerSize = CGSize(width: 44.0, height: 44.0) + if let sendPaidMessageStars = interfaceState.sendPaidMessageStars { + self.sendIconNode.alpha = 0.0 + self.textNode.isHidden = false + + var amount = sendPaidMessageStars.value + if let forwardedCount = interfaceState.interfaceState.forwardMessageIds?.count, forwardedCount > 0 { + amount = sendPaidMessageStars.value * Int64(forwardedCount) + if interfaceState.interfaceState.effectiveInputState.inputText.length > 0 { + amount += sendPaidMessageStars.value + } + } + + let text = "\(amount)" + let font = Font.with(size: 17.0, design: .round, weight: .semibold, traits: .monospacedNumbers) + let badgeString = NSMutableAttributedString(string: "⭐️ ", font: font, textColor: interfaceState.theme.chat.inputPanel.actionControlForegroundColor) + if let range = badgeString.string.range(of: "⭐️") { + badgeString.addAttribute(.attachment, value: PresentationResourcesChat.chatPlaceholderStarIcon(interfaceState.theme)!, range: NSRange(range, in: badgeString.string)) + badgeString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: badgeString.string)) + } + var segments: [AnimatedCountLabelNode.Segment] = [] + segments.append(.text(0, badgeString)) + for char in text { + if let intValue = Int(String(char)) { + segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: interfaceState.theme.chat.inputPanel.actionControlForegroundColor))) + } + } + self.textNode.segments = segments + + let textSize = self.textNode.updateLayout(size: CGSize(width: 100.0, height: 100.0), animated: transition.isAnimated) + let buttonInset: CGFloat = 14.0 + innerSize.width = textSize.width + buttonInset * 2.0 + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((innerSize.height - textSize.height) / 2.0)), size: textSize)) + } else { + self.sendIconNode.alpha = 1.0 + self.textNode.isHidden = true + } + + transition.updateFrame(node: self.sendButton, frame: CGRect(origin: CGPoint(x: width - rightInset - innerSize.width + 1.0 - UIScreenPixel, y: 1.0 + UIScreenPixel), size: innerSize)) + let backgroundSize = CGSize(width: innerSize.width - 11.0, height: 33.0) + let backgroundFrame = CGRect(origin: CGPoint(x: 5.0, y: floorToScreenPixels((innerSize.height - backgroundSize.height) / 2.0)), size: backgroundSize) + transition.updateFrame(node: self.sendBackgroundNode, frame: backgroundFrame) + self.sendBackgroundNode.cornerRadius = backgroundSize.height / 2.0 + + if let icon = self.sendIconNode.image { + transition.updateFrame(node: self.sendIconNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((innerSize.width - icon.size.width) / 2.0), y: floorToScreenPixels((innerSize.height - icon.size.height) / 2.0)), size: icon.size)) + } + if self.presentationInterfaceState != interfaceState { var updateWaveform = false if self.presentationInterfaceState?.interfaceState.mediaDraftState != interfaceState.interfaceState.mediaDraftState { @@ -346,7 +412,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { ), environment: {}, forceUpdate: false, - containerSize: CGSize(width: min(424, width - leftInset - rightInset - 45.0 * 2.0), height: 33.0) + containerSize: CGSize(width: min(424, width - leftInset - rightInset - 45.0 - innerSize.width - 1.0), height: 33.0) ) if let view = self.scrubber.view { @@ -364,11 +430,10 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } let panelHeight = defaultHeight(metrics: metrics) - transition.updateFrame(node: self.deleteButton, frame: CGRect(origin: CGPoint(x: leftInset + 2.0 - UIScreenPixel, y: 1), size: CGSize(width: 40.0, height: 40))) - transition.updateFrame(node: self.sendButton, frame: CGRect(origin: CGPoint(x: width - rightInset - 43.0 - UIScreenPixel, y: 2 - UIScreenPixel), size: CGSize(width: 44.0, height: 44))) + self.binNode.frame = self.deleteButton.bounds - + var viewOnceOffset: CGFloat = 0.0 if interfaceState.interfaceState.replyMessageSubject != nil { viewOnceOffset = -35.0 @@ -413,11 +478,11 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { transition.updateFrame(node: self.playButton, frame: CGRect(origin: CGPoint(x: leftInset + 52.0, y: 10.0), size: CGSize(width: 26.0, height: 26.0))) self.playPauseIconNode.frame = CGRect(origin: CGPoint(x: -2.0, y: -1.0), size: CGSize(width: 26.0, height: 26.0)) - let waveformBackgroundFrame = CGRect(origin: CGPoint(x: leftInset + 45.0, y: 7.0 - UIScreenPixel), size: CGSize(width: width - leftInset - rightInset - 90.0, height: 33.0)) + let waveformBackgroundFrame = CGRect(origin: CGPoint(x: leftInset + 45.0, y: 7.0 - UIScreenPixel), size: CGSize(width: width - leftInset - rightInset - 45.0 - innerSize.width - 1.0, height: 33.0)) transition.updateFrame(node: self.waveformBackgroundNode, frame: waveformBackgroundFrame) - transition.updateFrame(node: self.waveformButton, frame: CGRect(origin: CGPoint(x: leftInset + 45.0, y: 0.0), size: CGSize(width: width - leftInset - rightInset - 90.0, height: panelHeight))) - transition.updateFrame(node: self.waveformScrubberNode, frame: CGRect(origin: CGPoint(x: leftInset + 45.0 + 35.0, y: 7.0 + floor((33.0 - 13.0) / 2.0)), size: CGSize(width: width - leftInset - rightInset - 90.0 - 45.0 - 40.0, height: 13.0))) - transition.updateFrame(node: self.durationLabel, frame: CGRect(origin: CGPoint(x: width - rightInset - 90.0 - 4.0, y: 15.0), size: CGSize(width: 35.0, height: 20.0))) + transition.updateFrame(node: self.waveformButton, frame: CGRect(origin: CGPoint(x: leftInset + 45.0, y: 0.0), size: CGSize(width: width - leftInset - rightInset - 45.0 - innerSize.width - 1.0, height: panelHeight))) + transition.updateFrame(node: self.waveformScrubberNode, frame: CGRect(origin: CGPoint(x: leftInset + 45.0 + 35.0, y: 7.0 + floor((33.0 - 13.0) / 2.0)), size: CGSize(width: width - leftInset - rightInset - 45.0 - innerSize.width - 1.0 - 45.0 - 40.0, height: 13.0))) + transition.updateFrame(node: self.durationLabel, frame: CGRect(origin: CGPoint(x: width - rightInset - 45.0 - innerSize.width - 1.0 - 4.0, y: 15.0), size: CGSize(width: 35.0, height: 20.0))) prevInputPanelNode?.frame = CGRect(origin: .zero, size: CGSize(width: width, height: panelHeight)) if let prevTextInputPanelNode = self.prevInputPanelNode as? ChatTextInputPanelNode { diff --git a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift index f0c69e4bb0..7eac909522 100644 --- a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift @@ -350,13 +350,16 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { private let closeButton: HighlightableButtonNode private var buttons: [(ChatReportPeerTitleButton, UIButton)] = [] private let textNode: ImmediateTextNode - private var emojiStatusTextNode: TextNodeWithEntities? + private var emojiStatusTextNode: ImmediateTextNodeWithEntities? + private let emojiSeparatorNode: ASDisplayNode private var theme: PresentationTheme? private var inviteInfoNode: ChatInfoTitlePanelInviteInfoNode? private var peerNearbyInfoNode: ChatInfoTitlePanelPeerNearbyInfoNode? + private var cachedChevronImage: (UIImage, PresentationTheme)? + private var tapGestureRecognizer: UITapGestureRecognizer? init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) { @@ -367,6 +370,9 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true + self.emojiSeparatorNode = ASDisplayNode() + self.emojiSeparatorNode.isLayerBacked = true + self.closeButton = HighlightableButtonNode() self.closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) self.closeButton.displaysAsynchronously = false @@ -378,6 +384,7 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { super.init() self.addSubnode(self.separatorNode) + self.addSubnode(self.emojiSeparatorNode) self.addSubnode(self.textNode) self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) @@ -397,12 +404,37 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { self.interfaceInteraction?.presentChatRequestAdminInfo() } + private func openPremiumEmojiStatusDemo() { + guard let navigationController = self.interfaceInteraction?.getNavigationController() 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) + } + navigationController.pushViewController(controller) + } + } + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult { if interfaceState.theme !== self.theme { self.theme = interfaceState.theme self.closeButton.setImage(PresentationResourcesChat.chatInputPanelEncircledCloseIconImage(interfaceState.theme), for: []) self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor + self.emojiSeparatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor } var panelHeight: CGFloat = 40.0 @@ -556,55 +588,70 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { #endif*/ if let emojiStatus = emojiStatus { - let emojiStatusTextNode: TextNodeWithEntities + 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))) + + let emojiStatusTextNode: ImmediateTextNodeWithEntities if let current = self.emojiStatusTextNode { emojiStatusTextNode = current } else { - emojiStatusTextNode = TextNodeWithEntities() + emojiStatusTextNode = ImmediateTextNodeWithEntities() + emojiStatusTextNode.maximumNumberOfLines = 0 + emojiStatusTextNode.textAlignment = .center + emojiStatusTextNode.linkHighlightInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0) + emojiStatusTextNode.highlightAttributeAction = { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + } + emojiStatusTextNode.tapAttributeAction = { [weak self] attributes, _ in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + self?.openPremiumEmojiStatusDemo() + } + } self.emojiStatusTextNode = emojiStatusTextNode - self.addSubnode(emojiStatusTextNode.textNode) + self.addSubnode(emojiStatusTextNode) } - let plainText = interfaceState.strings.Chat_PanelCustomStatusInfo(".") - let attributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: plainText.string, font: Font.regular(12.0), textColor: interfaceState.theme.rootController.navigationBar.secondaryTextColor, paragraphAlignment: .center)) - for range in plainText.ranges { - attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: emojiStatus.fileId, file: nil), range: range.range) + if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== interfaceState.theme { + self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: interfaceState.theme.rootController.navigationBar.accentTextColor)!, interfaceState.theme) } - let makeEmojiStatusLayout = TextNodeWithEntities.asyncLayout(emojiStatusTextNode) - let (emojiStatusLayout, emojiStatusApply) = makeEmojiStatusLayout(TextNodeLayoutArguments( - attributedString: attributedText, - backgroundColor: nil, - minimumNumberOfLines: 0, - maximumNumberOfLines: 0, - truncationType: .end, - constrainedSize: CGSize(width: width - leftInset * 2.0 - 8.0 * 2.0, height: CGFloat.greatestFiniteMagnitude), - alignment: .center, - verticalAlignment: .top, - lineSpacing: 0.0, - cutout: nil, - insets: UIEdgeInsets(), - lineColor: nil, - textShadowColor: nil, - textStroke: nil, - displaySpoilers: false, - displayEmbeddedItemsUnderSpoilers: false - )) - let _ = emojiStatusApply(TextNodeWithEntities.Arguments( + let plainText = interfaceState.strings.Chat_PanelCustomStatusShortInfo("#").string + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(12.0), textColor: interfaceState.theme.rootController.navigationBar.secondaryTextColor), bold: MarkdownAttributeSet(font: Font.semibold(12.0), textColor: interfaceState.theme.rootController.navigationBar.secondaryTextColor), link: MarkdownAttributeSet(font: Font.regular(12.0), textColor: interfaceState.theme.rootController.navigationBar.accentTextColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + let attributedString = parseMarkdownIntoAttributedString(plainText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString + if let range = attributedString.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 { + attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) + } + if let range = attributedString.string.range(of: "#") { + attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: emojiStatus.fileId, file: nil), range: NSRange(range, in: attributedString.string)) + } + emojiStatusTextNode.attributedText = attributedString + emojiStatusTextNode.arguments = TextNodeWithEntities.Arguments( context: self.context, cache: self.animationCache, renderer: self.animationRenderer, placeholderColor: interfaceState.theme.list.mediaPlaceholderColor, attemptSynchronous: false - )) - transition.updateFrame(node: emojiStatusTextNode.textNode, frame: CGRect(origin: CGPoint(x: floor((width - emojiStatusLayout.size.width) / 2.0), y: panelHeight), size: emojiStatusLayout.size)) - panelHeight += emojiStatusLayout.size.height + 8.0 + ) + emojiStatusTextNode.linkHighlightColor = interfaceState.theme.list.itemAccentColor.withAlphaComponent(0.1) - emojiStatusTextNode.visibilityRect = .infinite + let emojiStatusTextSize = emojiStatusTextNode.updateLayout(CGSize(width: width - leftInset * 2.0 - 8.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)) + transition.updateFrame(node: emojiStatusTextNode, frame: CGRect(origin: CGPoint(x: floor((width - emojiStatusTextSize.width) / 2.0), y: panelHeight + 10.0), size: emojiStatusTextSize)) + panelHeight += emojiStatusTextSize.height + 20.0 + + emojiStatusTextNode.visibility = true } else { + self.emojiSeparatorNode.isHidden = true + if let emojiStatusTextNode = self.emojiStatusTextNode { self.emojiStatusTextNode = nil - emojiStatusTextNode.textNode.removeFromSupernode() + emojiStatusTextNode.removeFromSupernode() } } @@ -717,7 +764,9 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { return result } } - if point.y > 40.0 { + if let _ = self.emojiStatusTextNode { + + } else if point.y > 40.0 { return nil } return super.hitTest(point, with: event) diff --git a/submodules/TelegramUI/Sources/ChatStarsRequiredInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatStarsRequiredInputPanelNode.swift deleted file mode 100644 index f7e73e7612..0000000000 --- a/submodules/TelegramUI/Sources/ChatStarsRequiredInputPanelNode.swift +++ /dev/null @@ -1,150 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import TelegramCore -import Postbox -import SwiftSignalKit -import TelegramNotices -import TelegramPresentationData -import ActivityIndicator -import ChatPresentationInterfaceState -import ChatInputPanelNode -import ComponentFlow -import MultilineTextComponent -import MultilineTextWithEntitiesComponent -import PlainButtonComponent -import ComponentDisplayAdapters -import AccountContext -import TextFormat - -private let labelFont = Font.regular(15.0) - -final class ChatStarsRequiredInputPanelNode: ChatInputPanelNode { - private struct Params: Equatable { - var width: CGFloat - var leftInset: CGFloat - var rightInset: CGFloat - var bottomInset: CGFloat - var additionalSideInsets: UIEdgeInsets - var maxHeight: CGFloat - var isSecondary: Bool - var interfaceState: ChatPresentationInterfaceState - var metrics: LayoutMetrics - var isMediaInputExpanded: Bool - - init(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) { - self.width = width - self.leftInset = leftInset - self.rightInset = rightInset - self.bottomInset = bottomInset - self.additionalSideInsets = additionalSideInsets - self.maxHeight = maxHeight - self.isSecondary = isSecondary - self.interfaceState = interfaceState - self.metrics = metrics - self.isMediaInputExpanded = isMediaInputExpanded - } - } - - private struct Layout { - var params: Params - var height: CGFloat - - init(params: Params, height: CGFloat) { - self.params = params - self.height = height - } - } - - private let button = ComponentView() - - private var params: Params? - private var currentLayout: Layout? - - override var interfaceInteraction: ChatPanelInterfaceInteraction? { - didSet { - } - } - - init(theme: PresentationTheme) { - super.init() - } - - deinit { - } - - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { - let params = Params(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded) - if let currentLayout = self.currentLayout, currentLayout.params == params { - return currentLayout.height - } - - let height = self.update(params: params, transition: ComponentTransition(transition)) - self.currentLayout = Layout(params: params, height: height) - - return height - } - - private func update(params: Params, transition: ComponentTransition) -> CGFloat { - let height: CGFloat - if case .regular = params.metrics.widthClass { - height = 49.0 - } else { - height = 45.0 - } - - let price = params.interfaceState.sendPaidMessageStars?.value ?? 0 - - //TODO:localize - let attributedText = NSMutableAttributedString(string: "Pay ⭐️\(price) for 1 Message", font: Font.regular(17.0), textColor: params.interfaceState.theme.rootController.navigationBar.accentTextColor) - if let range = attributedText.string.range(of: "⭐️") { - attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: NSRange(range, in: attributedText.string)) - attributedText.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedText.string)) - } - - var buttonContents: [AnyComponentWithIdentity] = [] - if let context = self.context { - buttonContents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent( - MultilineTextWithEntitiesComponent( - context: context, - animationCache: context.animationCache, - animationRenderer: context.animationRenderer, - placeholderColor: .white, - text: .plain(attributedText) - ) - ))) - } - - let size = CGSize(width: params.width - params.additionalSideInsets.left * 2.0 - params.leftInset * 2.0, height: height) - let buttonSize = self.button.update( - transition: .immediate, - component: AnyComponent(PlainButtonComponent( - content: AnyComponent(VStack(buttonContents, spacing: 1.0)), - effectAlignment: .center, - minSize: size, - action: { [weak self] in - guard let self else { - return - } - self.interfaceInteraction?.openMessagePayment() - }, - animateScale: false - )), - environment: {}, - containerSize: size - ) - if let buttonView = self.button.view { - if buttonView.superview == nil { - self.view.addSubview(buttonView) - } - transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(), size: buttonSize)) - } - - return height - } - - override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - return defaultHeight(metrics: metrics) - } -} diff --git a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift b/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift index 318de27a4d..5d83f3b04b 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift @@ -14,6 +14,7 @@ import ChatTextInputMediaRecordingButton import ChatSendButtonRadialStatusNode import ChatSendMessageActionUI import ComponentFlow +import AnimatedCountLabelNode private final class EffectBadgeView: UIView { private let context: AccountContext @@ -138,6 +139,9 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode? var sendButtonHasApplyIcon = false var animatingSendButton = false + + let textNode: ImmediateAnimatedCountLabelNode + let expandMediaInputButton: HighlightableButtonNode private var effectBadgeView: EffectBadgeView? @@ -173,6 +177,9 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction self.backdropNode = ChatMessageBubbleBackdrop() self.sendButton = HighlightTrackingButtonNode(pointerStyle: nil) + self.textNode = ImmediateAnimatedCountLabelNode() + self.textNode.isUserInteractionEnabled = false + self.expandMediaInputButton = HighlightableButtonNode(pointerStyle: .circle(36.0)) super.init() @@ -211,6 +218,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction self.backgroundNode.addSubnode(self.backdropNode) } self.sendContainerNode.addSubnode(self.sendButton) + self.sendContainerNode.addSubnode(self.textNode) self.addSubnode(self.expandMediaInputButton) } @@ -260,20 +268,61 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction } } - func updateLayout(size: CGSize, isMediaInputExpanded: Bool, currentMessageEffectId: Int64?, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { + func updateLayout(size: CGSize, isMediaInputExpanded: Bool, showTitle: Bool, currentMessageEffectId: Int64?, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGSize { self.validLayout = size + + var innerSize = size + if let sendPaidMessageStars = interfaceState.sendPaidMessageStars { + self.sendButton.imageNode.alpha = 0.0 + self.textNode.isHidden = false + + var amount = sendPaidMessageStars.value + if let forwardedCount = interfaceState.interfaceState.forwardMessageIds?.count, forwardedCount > 0 { + amount = sendPaidMessageStars.value * Int64(forwardedCount) + if interfaceState.interfaceState.effectiveInputState.inputText.length > 0 { + amount += sendPaidMessageStars.value + } + } + + let text = "\(amount)" + let font = Font.with(size: 17.0, design: .round, weight: .semibold, traits: .monospacedNumbers) + let badgeString = NSMutableAttributedString(string: "⭐️ ", font: font, textColor: interfaceState.theme.chat.inputPanel.actionControlForegroundColor) + if let range = badgeString.string.range(of: "⭐️") { + badgeString.addAttribute(.attachment, value: PresentationResourcesChat.chatPlaceholderStarIcon(interfaceState.theme)!, range: NSRange(range, in: badgeString.string)) + badgeString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: badgeString.string)) + } + var segments: [AnimatedCountLabelNode.Segment] = [] + segments.append(.text(0, badgeString)) + for char in text { + if let intValue = Int(String(char)) { + segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: interfaceState.theme.chat.inputPanel.actionControlForegroundColor))) + } + } + self.textNode.segments = segments + + let textSize = self.textNode.updateLayout(size: CGSize(width: 100.0, height: 100.0), animated: transition.isAnimated) + let buttonInset: CGFloat = 14.0 + if showTitle { + innerSize.width = textSize.width + buttonInset * 2.0 + } + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: showTitle ? 5.0 + 7.0 : floorToScreenPixels((innerSize.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize)) + } else { + self.sendButton.imageNode.alpha = 1.0 + self.textNode.isHidden = true + } + transition.updateFrame(layer: self.micButton.layer, frame: CGRect(origin: CGPoint(), size: size)) self.micButton.layoutItems() - transition.updateFrame(layer: self.sendButton.layer, frame: CGRect(origin: CGPoint(), size: size)) - transition.updateFrame(node: self.sendContainerNode, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrame(layer: self.sendButton.layer, frame: CGRect(origin: CGPoint(), size: innerSize)) + transition.updateFrame(node: self.sendContainerNode, frame: CGRect(origin: CGPoint(), size: innerSize)) - let backgroundSize = CGSize(width: 33.0, height: 33.0) - let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - backgroundSize.width) / 2.0), y: floorToScreenPixels((size.height - backgroundSize.height) / 2.0)), size: backgroundSize) + let backgroundSize = CGSize(width: innerSize.width - 11.0, height: 33.0) + let backgroundFrame = CGRect(origin: CGPoint(x: showTitle ? 5.0 + UIScreenPixel : floorToScreenPixels((size.width - backgroundSize.width) / 2.0), y: floorToScreenPixels((size.height - backgroundSize.height) / 2.0)), size: backgroundSize) transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - self.backgroundNode.cornerRadius = backgroundSize.width / 2.0 + self.backgroundNode.cornerRadius = backgroundSize.height / 2.0 - transition.updateFrame(node: self.backdropNode, frame: CGRect(origin: CGPoint(x: -2.0, y: -2.0), size: CGSize(width: size.width + 12.0, height: size.height + 2.0))) + transition.updateFrame(node: self.backdropNode, frame: CGRect(origin: CGPoint(x: -2.0, y: -2.0), size: CGSize(width: innerSize.width + 12.0, height: size.height + 2.0))) if let (rect, containerSize) = self.absoluteRect { self.backdropNode.update(rect: rect, within: containerSize) } @@ -303,6 +352,8 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction effectBadgeView?.removeFromSuperview() }) } + + return innerSize } func updateAccessibility() { diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 413ed336ca..a455ae4732 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -43,6 +43,7 @@ import ChatInputPanelNode import TelegramNotices import AnimatedCountLabelNode import TelegramStringFormatting +import TextNodeWithEntities private let accessoryButtonFont = Font.medium(14.0) private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers]) @@ -525,7 +526,7 @@ private func makeTextInputTheme(context: AccountContext, interfaceState: ChatPre class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, ChatInputTextNodeDelegate { let clippingNode: ASDisplayNode - var textPlaceholderNode: ImmediateTextNode + var textPlaceholderNode: ImmediateTextNodeWithEntities var textLockIconNode: ASImageNode? var contextPlaceholderNode: TextNode? var slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode? @@ -820,7 +821,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.textInputBackgroundNode = ASImageNode() self.textInputBackgroundNode.displaysAsynchronously = false self.textInputBackgroundNode.displayWithoutProcessing = true - self.textPlaceholderNode = ImmediateTextNode() + + self.textPlaceholderNode = ImmediateTextNodeWithEntities() + self.textPlaceholderNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: .clear, + attemptSynchronous: true + ) self.textPlaceholderNode.contentMode = .topLeft self.textPlaceholderNode.contentsScale = UIScreenScale self.textPlaceholderNode.maximumNumberOfLines = 1 @@ -1721,6 +1730,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } var updatedPlaceholder: String? + var placeholderHasStar = false let themeUpdated = self.presentationInterfaceState?.theme !== interfaceState.theme @@ -1916,9 +1926,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch placeholder = interfaceState.strings.Chat_InputPlaceholderMessageInTopic(forumTopicData.title).string } } else { - if interfaceState.acknowledgedPaidMessage { - //TODO:localize - placeholder = "Prepaid Message" + if let sendPaidMessageStars = interfaceState.sendPaidMessageStars { + placeholder = interfaceState.strings.Chat_InputTextPaidMessagePlaceholder(" # \(presentationStringsFormattedNumber(Int32(sendPaidMessageStars.value), interfaceState.dateTimeFormat.groupingSeparator))").string + placeholderHasStar = true } else { placeholder = interfaceState.strings.Conversation_InputTextPlaceholder } @@ -2481,15 +2491,27 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.updateCounterTextNode(transition: transition) - let actionButtonsFrame = CGRect(origin: CGPoint(x: hideOffset.x + width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset, y: hideOffset.y + panelHeight - minimalHeight), size: CGSize(width: 44.0, height: minimalHeight)) + if inputHasText || self.extendedSearchLayout { + hideMicButton = true + } + + self.updateActionButtons(hasText: inputHasText, hideMicButton: hideMicButton, animated: transition.isAnimated) + + var actionButtonsSize = CGSize(width: 44.0, height: minimalHeight) + if let presentationInterfaceState = self.presentationInterfaceState { + var showTitle = false + if let _ = presentationInterfaceState.sendPaidMessageStars, !self.actionButtons.sendContainerNode.alpha.isZero { + showTitle = true + } + actionButtonsSize = self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), isMediaInputExpanded: isMediaInputExpanded, showTitle: showTitle, currentMessageEffectId: presentationInterfaceState.interfaceState.sendMessageEffect, transition: transition, interfaceState: presentationInterfaceState) + } + + let actionButtonsFrame = CGRect(origin: CGPoint(x: hideOffset.x + width - rightInset - actionButtonsSize.width + 1 - UIScreenPixel + composeButtonsOffset, y: hideOffset.y + panelHeight - minimalHeight), size: actionButtonsSize) transition.updateFrame(node: self.actionButtons, frame: actionButtonsFrame) if let (rect, containerSize) = self.absoluteRect { self.actionButtons.updateAbsoluteRect(CGRect(x: rect.origin.x + actionButtonsFrame.origin.x, y: rect.origin.y + actionButtonsFrame.origin.y, width: actionButtonsFrame.width, height: actionButtonsFrame.height), within: containerSize, transition: transition) } - if let presentationInterfaceState = self.presentationInterfaceState { - self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), isMediaInputExpanded: isMediaInputExpanded, currentMessageEffectId: presentationInterfaceState.interfaceState.sendMessageEffect, transition: transition, interfaceState: presentationInterfaceState) - } let slowModeButtonFrame = CGRect(origin: CGPoint(x: hideOffset.x + width - rightInset - 5.0 - slowModeButtonSize.width + composeButtonsOffset, y: hideOffset.y + panelHeight - minimalHeight + 6.0), size: slowModeButtonSize) transition.updateFrame(node: self.slowModeButton, frame: slowModeButtonFrame) @@ -2533,6 +2555,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch let searchLayoutClearButtonSize = CGSize(width: 44.0, height: minimalHeight) var textFieldInsets = self.textFieldInsets(metrics: metrics) + if actionButtonsSize.width > 44.0 { + textFieldInsets.right = actionButtonsSize.width - 2.0 + } if additionalSideInsets.right > 0.0 { textFieldInsets.right += additionalSideInsets.right / 3.0 } @@ -2657,7 +2682,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch let currentPlaceholder = updatedPlaceholder ?? self.currentPlaceholder ?? "" self.currentPlaceholder = currentPlaceholder let baseFontSize = max(minInputFontSize, interfaceState.fontSize.baseDisplaySize) - self.textPlaceholderNode.attributedText = NSAttributedString(string: currentPlaceholder, font: Font.regular(baseFontSize), textColor: interfaceState.theme.chat.inputPanel.inputPlaceholderColor) + + let attributedPlaceholder = NSMutableAttributedString(string: currentPlaceholder, font:Font.regular(baseFontSize), textColor: interfaceState.theme.chat.inputPanel.inputPlaceholderColor) + if placeholderHasStar, let range = attributedPlaceholder.string.range(of: "#") { + attributedPlaceholder.addAttribute(.attachment, value: PresentationResourcesChat.chatPlaceholderStarIcon(interfaceState.theme)!, range: NSRange(range, in: attributedPlaceholder.string)) + attributedPlaceholder.addAttribute(.foregroundColor, value: interfaceState.theme.chat.inputPanel.inputPlaceholderColor, range: NSRange(range, in: attributedPlaceholder.string)) + attributedPlaceholder.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedPlaceholder.string)) + } + + self.textPlaceholderNode.attributedText = attributedPlaceholder self.textInputNode?.textView.accessibilityHint = currentPlaceholder let placeholderSize = self.textPlaceholderNode.updateLayout(CGSize(width: textPlaceholderMaxWidth, height: CGFloat.greatestFiniteMagnitude)) if transition.isAnimated, let snapshotLayer = self.textPlaceholderNode.layer.snapshotContentTree() { @@ -2715,11 +2748,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch }) } } - - if inputHasText || self.extendedSearchLayout { - hideMicButton = true - } - + let mediaInputDisabled: Bool if !interfaceState.voiceMessagesAvailable { mediaInputDisabled = true @@ -2740,8 +2769,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.actionButtons.micButton.fadeDisabled = mediaInputDisabled || mediaInputIsActive - self.updateActionButtons(hasText: inputHasText, hideMicButton: hideMicButton, animated: transition.isAnimated) - var viewOnceIsVisible = false if let recordingState = interfaceState.inputTextPanelState.mediaRecordingState { if case let .audio(_, isLocked) = recordingState { @@ -2890,7 +2917,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } transition.updateAlpha(node: self.viewOnceButton, alpha: viewOnceIsVisible ? 1.0 : 0.0) transition.updateTransformScale(node: self.viewOnceButton, scale: viewOnceIsVisible ? 1.0 : 0.01) - if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.id != interfaceState.accountPeerId && user.botInfo == nil { + if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.id != interfaceState.accountPeerId && user.botInfo == nil && interfaceState.sendPaidMessageStars == nil { self.viewOnceButton.isHidden = false } else { self.viewOnceButton.isHidden = true @@ -4494,7 +4521,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch let aspectRatio = min(image.size.width, image.size.height) / maxSide if isMemoji || (imageHasTransparency(image) && aspectRatio > 0.2) { self.paste(.sticker(image, isMemoji)) - return true + return false } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index b7542b0915..eeeee7f182 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2450,6 +2450,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSource = .messageEffects case .animatedEmoji: mappedSource = .animatedEmoji + case .paidMessages: + mappedSource = .paidMessages } let controller = PremiumIntroScreen(context: context, source: mappedSource, modal: modal, forceDark: forceDark) controller.wasDismissed = dismissed @@ -2506,6 +2508,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSubject = .folderTags case .messageEffects: mappedSubject = .messageEffects + case .paidMessages: + mappedSubject = .paidMessages case .business: mappedSubject = .business buttonText = presentationData.strings.Chat_EmptyStateIntroFooterPremiumActionButton @@ -3433,6 +3437,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return MiniAppListScreen(context: context, initialData: initialData as! MiniAppListScreen.InitialData) } + public func makeIncomingMessagePrivacyScreen(context: AccountContext, value: GlobalPrivacySettings.NonContactChatsPrivacy, exceptions: SelectivePrivacySettings, update: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void) -> ViewController { + return incomingMessagePrivacyScreen(context: context, value: value, exceptions: exceptions, update: update) + } + public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) { openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, botPeer: botPeer, chatPeer: chatPeer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService, payload: payload) } diff --git a/third-party/flatc/Package.swift b/third-party/flatc/Package.swift new file mode 100644 index 0000000000..b4184bdbac --- /dev/null +++ b/third-party/flatc/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version:5.5 +import PackageDescription + +let package = Package( + name: "FlatBuffersBuilder", + platforms: [ + .macOS(.v12) + ], + products: [ + .plugin( + name: "FlatBuffersPlugin", + targets: ["FlatBuffersPlugin"] + ) + ], + dependencies: [], + targets: [ + .binaryTarget( + name: "flatc", + url: "https://github.com/google/flatbuffers/releases/download/v23.5.26/Mac.flatc.binary.zip", + checksum: "d65628c225ef26e0386df003fe47d6b3ec8775c586d7dae1a9ef469a0a9906f1" + ), + .plugin( + name: "FlatBuffersPlugin", + capability: .buildTool(), + dependencies: ["flatc"] + ) + ] +)