diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index e7b3c4abfb..f189e1295f 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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,21 @@ 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 >]()"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index ce848cbac1..686709c540 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 diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 18bd99f8fd..e237db8ebb 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -134,6 +134,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/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 0ea96bd194..6d291bf928 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/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 d2a95292e1..daadeca6ed 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/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..b1c1d646c5 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, String) case messagePriceInfo(PresentationTheme, String) case unrestrictBoostersSwitch(PresentationTheme, String, Bool) @@ -425,7 +425,7 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry { 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 + return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, minValue: 1, maxValue: 10000, value: value, price: price, sectionId: self.section, updated: { value in arguments.updateStarsAmount(StarsAmount(value: value, nanos: 0)) }) case let .messagePriceInfo(_, value): @@ -720,23 +720,24 @@ 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 { + if 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 = "" 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))" + 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, price)) + entries.append(.messagePriceInfo(presentationData.theme, presentationData.strings.GroupInfo_Permissions_MessagePriceInfo(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 +877,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 +1256,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/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..0009407f80 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift @@ -129,7 +129,11 @@ private enum GlobalAutoremoveEntry: ItemListNodeEntry { 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: { - arguments.updateValue(.paidMessages(StarsAmount(value: 400, nanos: 0))) + if isEnabled { + arguments.updateValue(.paidMessages(StarsAmount(value: 400, nanos: 0))) + } else { + arguments.disabledValuePressed() + } }) case let .footer(value): let text: String @@ -146,7 +150,7 @@ 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 + return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, minValue: 1, maxValue: 10000, value: value, price: price, sectionId: self.section, updated: { value in arguments.updateValue(.paidMessages(StarsAmount(value: value, nanos: 0))) }) case let .priceInfo(value): @@ -168,12 +172,12 @@ 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(.optionPremium(value: state.updatedValue, isEnabled: enableSetting)) entries.append(.optionChargeForMessages(value: state.updatedValue, isEnabled: isPremium)) if case let .paidMessages(amount) = state.updatedValue { @@ -375,7 +379,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 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..def38e6cfa 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,19 @@ 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 + } + var mediaParameters: ShareControllerSubject.MediaParameters? if case let .media(_, parameters) = self.subject { mediaParameters = parameters @@ -682,7 +690,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 +1248,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: 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 |> 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 +1284,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: Int64?) -> [StandaloneSendEnqueueMessage] { return messages.map { message in var message = message if !showNames { @@ -1278,6 +1296,7 @@ public final class ShareController: ViewController { if silently { message.isSilent = true } + message.sendPaidMessageStars = sendPaidMessageStars.flatMap { StarsAmount(value: $0, nanos: 0) } return message } } @@ -1325,7 +1344,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 +1405,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 +1470,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 +1530,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 +1640,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 +1699,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 +1819,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, @@ -1880,12 +1899,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: 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, 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 +1938,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: Int64?) -> [EnqueueMessage] { return messages.map { message in return message.withUpdatedAttributes({ attributes in var attributes = attributes @@ -1907,6 +1948,9 @@ public final class ShareController: ViewController { if silently { attributes.append(NotificationInfoMessageAttribute(flags: .muted)) } + if let sendPaidMessageStars { + attributes.append(PaidStarsMessageAttribute(stars: StarsAmount(value: sendPaidMessageStars, nanos: 0), postponeSending: false)) + } return attributes }) } @@ -1949,7 +1993,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 +2027,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 +2064,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 +2095,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 +2189,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 +2223,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,7 +2318,7 @@ 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): @@ -2458,7 +2502,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 +2512,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 +2531,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 +2539,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/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/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/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_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/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index d06770b90d..cda12a4577 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -349,6 +349,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/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/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift index 3579116832..e45c8961d1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift @@ -773,14 +773,14 @@ public final class ChatInlineSearchResultsListComponent: Component { if let forwardInfo = message.forwardInfo { effectiveAuthor = forwardInfo.author.flatMap(EnginePeer.init) if effectiveAuthor == nil, let authorSignature = forwardInfo.authorSignature { - effectiveAuthor = EnginePeer(TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)) + effectiveAuthor = EnginePeer(TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)) } } if let sourceAuthorInfo = message._asMessage().sourceAuthorInfo { if let originalAuthor = sourceAuthorInfo.originalAuthor, let peer = message.peers[originalAuthor] { effectiveAuthor = EnginePeer(peer) } else if let authorSignature = sourceAuthorInfo.originalAuthorName { - effectiveAuthor = EnginePeer(TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)) + effectiveAuthor = EnginePeer(TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)) } } if effectiveAuthor == nil { 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/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 6d284786e1..297d1a9980 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -236,7 +236,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { if let peer = forwardInfo.author { author = peer } else if let authorSignature = forwardInfo.authorSignature { - author = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil) + author = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil) } } 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 222e34b12f..67f242a7ef 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 } } @@ -1521,11 +1526,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let sourceAuthorInfo, let originalAuthorId = sourceAuthorInfo.originalAuthor, let peer = item.message.peers[originalAuthorId] { effectiveAuthor = peer } else if let sourceAuthorInfo, let originalAuthorName = sourceAuthorInfo.originalAuthorName { - effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(originalAuthorName.persistentHashValue % 32))), accessHash: nil, firstName: originalAuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil) + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(originalAuthorName.persistentHashValue % 32))), accessHash: nil, firstName: originalAuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil) } else { ignoreForward = true if effectiveAuthor == nil, let authorSignature = forwardInfo.authorSignature { - effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil) + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil) } } } @@ -1542,7 +1547,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI displayAuthorInfo = !mergedTop.merged && incoming } else if let forwardInfo = item.content.firstMessage.forwardInfo, forwardInfo.flags.contains(.isImported), let authorSignature = forwardInfo.authorSignature { ignoreForward = true - effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil) + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil) displayAuthorInfo = !mergedTop.merged && incoming } else if let _ = item.content.firstMessage.adAttribute, let author = item.content.firstMessage.author { ignoreForward = 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 @@ -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,9 +2996,17 @@ 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) + var containerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentNodeOriginY), size: size) + if size.height == 33.0 && detachedContentNodesHeight > 0.0 { + //TODO:unmock + containerFrame = containerFrame.offsetBy(dx: 0.0, dy: 2.0) + } contentNodeFramesPropertiesAndApply.append((containerFrame, properties, contentGroupId == nil, apply)) if contentProperties.neighborType == .media && unlockButtonPosition == nil { @@ -2998,7 +3018,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 +3181,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 +3189,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 +4070,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 +4104,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 675697a25d..d047936ec2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -2137,6 +2137,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 7fcfebe29d..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 } @@ -288,14 +293,14 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible if let forwardInfo = content.firstMessage.forwardInfo { effectiveAuthor = forwardInfo.author if effectiveAuthor == nil, let authorSignature = forwardInfo.authorSignature { - effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil) + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil) } } if let sourceAuthorInfo = content.firstMessage.sourceAuthorInfo { if let originalAuthor = sourceAuthorInfo.originalAuthor, let peer = content.firstMessage.peers[originalAuthor] { effectiveAuthor = peer } else if let authorSignature = sourceAuthorInfo.originalAuthorName { - effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil) + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil) } } if peerId.isVerificationCodes && effectiveAuthor == nil { 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 78% rename from submodules/TelegramUI/Sources/Chat/ChatMessagePaymentAlertController.swift rename to submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift index 3834ff6747..26dd0e00ff 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,14 +318,16 @@ 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) } @@ -346,66 +348,102 @@ private class ChatMessagePaymentAlertController: AlertController { 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 +452,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 +462,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 +492,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 +501,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/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift index df63a2158c..edd5cacd3d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift @@ -209,7 +209,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { if let peer = forwardInfo.author { author = peer } else if let authorSignature = forwardInfo.authorSignature { - author = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil) + author = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil) } } 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..3de9d47c32 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift @@ -0,0 +1,581 @@ +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 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 + + backgroundSize.width = max(backgroundSize.width, horizontalContentInset * 2.0 + (registrationDateTitleLayoutAndApply?.0.size.width ?? 0) + attributeSpacing + (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 + + backgroundSize.width = max(backgroundSize.width, horizontalContentInset * 2.0 + (phoneCountryTitleLayoutAndApply?.0.size.width ?? 0) + attributeSpacing + (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 + + backgroundSize.width = max(backgroundSize.width, horizontalContentInset * 2.0 + (locationCountryTitleLayoutAndApply?.0.size.width ?? 0) + attributeSpacing + (locationCountryValueLayoutAndApply?.0.size.width ?? 0)) + } else { + locationCountryTitleLayoutAndApply = nil + locationCountryValueLayoutAndApply = nil + } + + 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..ee6a362431 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, @@ -713,14 +714,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 +858,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 +891,7 @@ final class GiftOptionsScreenComponent: Component { default: title = strings.Gift_Options_Premium_Months(3) } - - //TODO:unmock + let _ = visibleItem.update( transition: itemTransition, component: AnyComponent( @@ -895,11 +900,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 +1053,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 +1229,7 @@ final class GiftOptionsScreenComponent: Component { botUrl: "", storeProductId: option.storeProductId ), + starsGiftOption: nil, storeProduct: nil, discount: nil ) @@ -1243,7 +1249,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 757165cd22..a2ab0915c0 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/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift index dd8c5dfb7f..787b697927 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift @@ -176,7 +176,7 @@ public final class LoadingOverlayNode: ASDisplayNode { let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) - let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)) + let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)) let timestamp1: Int32 = 100000 let peers: [EnginePeer.Id: EnginePeer] = [:] let interaction = ChatListNodeInteraction(context: context, animationCache: context.animationCache, animationRenderer: context.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in 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/Sources/SendInviteLinkScreen.swift b/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift index dcee906421..158d1accb0 100644 --- a/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift +++ b/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift @@ -1014,8 +1014,7 @@ public class SendInviteLinkScreen: ViewControllerComponentContainer { profileColor: user.profileColor, profileBackgroundEmojiId: user.profileBackgroundEmojiId, subscriberCount: user.subscriberCount, - verificationIconFileId: user.verificationIconFileId, - sendPaidMessageStars: nil + verificationIconFileId: user.verificationIconFileId )), canInviteWithPremium: canInviteWithPremium, premiumRequiredToContact: premiumRequiredToContact 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/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift index c6e35702dd..221bd2b1a3 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift @@ -225,12 +225,12 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode { var peers = SimpleDictionary() var messages = SimpleDictionary() - peers[authorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: messageItem.author, lastName: "", username: nil, phone: nil, photo: messageItem.photo, botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: messageItem.nameColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil) + peers[authorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: messageItem.author, lastName: "", username: nil, phone: nil, photo: messageItem.photo, botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: messageItem.nameColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil) let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3) if let (replyAuthor, text, replyColor) = messageItem.reply { - peers[replyAuthorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: replyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: replyColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil) + peers[replyAuthorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: replyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: replyColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil) messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[replyAuthorPeerId], text: text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) } diff --git a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionLoadingView.swift b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionLoadingView.swift index 574ad96acf..2c332f4e8d 100644 --- a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionLoadingView.swift +++ b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionLoadingView.swift @@ -159,7 +159,7 @@ final class PeerSelectionLoadingView: UIView { if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData { self.currentParams = (size, presentationData) - let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)) + let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)) let items = (0 ..< 1).map { _ -> ContactsPeerItem in return ContactsPeerItem( diff --git a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift index 394a65b183..301cbe8927 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift @@ -304,7 +304,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode { var peers = SimpleDictionary() let messages = SimpleDictionary() - peers[userPeerId] = TelegramUser(id: userPeerId, accessHash: nil, firstName: item.strings.Settings_QuickReactionSetup_DemoMessageAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil) + peers[userPeerId] = TelegramUser(id: userPeerId, accessHash: nil, firstName: item.strings.Settings_QuickReactionSetup_DemoMessageAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil) let messageText = item.strings.Settings_QuickReactionSetup_DemoMessageText diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index 34b81489e1..0a35bc76f5 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -958,12 +958,12 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate ) } - let selfPeer: EnginePeer = .user(TelegramUser(id: self.context.account.peerId, accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)) - let peer1: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)) - let peer2: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)) + let selfPeer: EnginePeer = .user(TelegramUser(id: self.context.account.peerId, accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)) + let peer1: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)) + let peer2: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)) let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)) - let peer3Author: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_AuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)) - let peer4: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)) + let peer3Author: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_AuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)) + let peer4: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)) let timestamp = self.referenceTimestamp @@ -1051,8 +1051,8 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate let otherPeerId = self.context.account.peerId var peers = SimpleDictionary() var messages = SimpleDictionary() - peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil) - peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil) + peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil) + peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil) var sampleMessages: [Message] = [] diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift index b62ee6ff48..6de7af8267 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift @@ -1513,8 +1513,8 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let replyAuthor = self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName - var messageAuthor: Peer = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_PreviewReplyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil) - let otherAuthor = TelegramUser(id: otherPeerId, accessHash: nil, firstName: replyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil) + var messageAuthor: Peer = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_PreviewReplyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil) + let otherAuthor = TelegramUser(id: otherPeerId, accessHash: nil, firstName: replyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil) peers[otherPeerId] = otherAuthor var messageAttributes: [MessageAttribute] = [] 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/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 02b9a3db5f..0677c3dd74 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -1937,7 +1937,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) 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/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..7a20044cdd 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -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 { @@ -2856,7 +2855,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] _ in + self?.sendMediaRecording(silentPosting: silentPosting, viewOnce: viewOnce) + }) }, displayRestrictedInfo: { [weak self] subject, displayType in guard let strongSelf = self else { return @@ -4397,30 +4398,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..4bd5b9db33 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,14 @@ extension ChatControllerImpl { updatedAction = .preview } + var sendImmediately = false + if let _ = self.presentationInterfaceState.sendPaidMessageStars { + if case .send = action { + updatedAction = .preview + } + sendImmediately = true + } + if let audioRecorderValue = self.audioRecorderValue { switch action { case .pause: @@ -296,6 +304,10 @@ extension ChatControllerImpl { strongSelf.recorderFeedback = nil strongSelf.updateDownButtonVisibility() strongSelf.recorderDataDisposable.set(nil) + + if sendImmediately { + strongSelf.interfaceInteraction?.sendRecordedMedia(false, false) + } } } })) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift new file mode 100644 index 0000000000..108a6a64ff --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift @@ -0,0 +1,123 @@ +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 { + 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 else { + return + } + if let dismissedAmount, dismissedAmount == sendPaidMessageStars.value { + 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,//self.updatedPresentationData, + peers: [peer], + count: count, + amount: sendPaidMessageStars, + totalAmount: nil, + navigationController: self.navigationController as? NavigationController, + completion: { [weak self] dontAskAgain in + guard let self, let starsContext = self.context.starsContext 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)) + } + }) + } + } + + 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..8974823684 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] _ 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) + 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) + strongSelf.sendMessages(transformedMessages) + } + }) + } else { + let transformedMessages = strongSelf.transformEnqueueMessages(messages) + 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) { @@ -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: true)) } if silentPosting || scheduleTime != nil { @@ -9330,7 +9350,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - 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] _ in + self?.commitEnqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, 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, 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 { diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 466e2412d0..4dd18915bb 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -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] _ in + self?.sendCurrentMessage() + }) + } else { + self.sendCurrentMessage() + } } } } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index 58afd63dd1..b127cbbb01 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] _ in + self?.sendMessages([message], media: true) + }) }) 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] _ 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]) + }) }) 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] _ 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] _ in + guard let strongSelf = self else { + return + } + strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) + }) } 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] _ in + guard let strongSelf = self else { + return + } + strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) + }) } }), 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] _ in + guard let strongSelf = self else { + return + } + strongSelf.sendMessages(messages) + }) } } } @@ -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] _ 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]) + }) }) strongSelf.effectiveNavigationController?.pushViewController(controller) strongSelf.chatDisplayNode.dismissInput() @@ -1621,7 +1661,12 @@ extension ChatControllerImpl { enqueueMessages.append(message) } } - strongSelf.sendMessages(enqueueMessages) + strongSelf.presentPaidMessageAlertIfNeeded(completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.sendMessages(enqueueMessages) + }) } 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] _ in + guard let strongSelf = self else { + return + } + strongSelf.sendMessages([message]) + }) } 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] _ in + guard let strongSelf = self else { + return + } + strongSelf.sendMessages([message]) + }) } }), 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..d6e62f58cd 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,18 +268,59 @@ 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))) if let (rect, containerSize) = self.absoluteRect { @@ -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 9024c256c8..e8175ac0a6 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,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch placeholder = interfaceState.strings.Chat_InputPlaceholderMessageInTopic(forumTopicData.title).string } } else { - if interfaceState.acknowledgedPaidMessage { + if let sendPaidMessageStars = interfaceState.sendPaidMessageStars { //TODO:localize - placeholder = "Prepaid Message" + placeholder = "Message for # \(presentationStringsFormattedNumber(Int32(sendPaidMessageStars.value), interfaceState.dateTimeFormat.groupingSeparator))" + placeholderHasStar = true } else { placeholder = interfaceState.strings.Conversation_InputTextPlaceholder } @@ -2481,15 +2492,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 +2556,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 +2683,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 +2749,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch }) } } - - if inputHasText || self.extendedSearchLayout { - hideMicButton = true - } - + let mediaInputDisabled: Bool if !interfaceState.voiceMessagesAvailable { mediaInputDisabled = true @@ -2740,8 +2770,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 +2918,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 +4522,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 26b98efe29..c21c2a2938 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -3469,6 +3469,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) }