From ef3512668c9967056e6e34b614c348c6bbbd0a24 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 5 Sep 2025 16:11:56 +0500 Subject: [PATCH] Various improvements --- .../GiftRemoveInfoAlertController.swift | 347 ++++++++++++++++++ .../Sources/GiftViewScreen.swift | 39 +- .../TelegramUI/Sources/AccountContext.swift | 14 +- .../TelegramUI/Sources/AppDelegate.swift | 54 ++- .../Sources/PostboxKeys.swift | 2 + 5 files changed, 433 insertions(+), 23 deletions(-) create mode 100644 submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftRemoveInfoAlertController.swift diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftRemoveInfoAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftRemoveInfoAlertController.swift new file mode 100644 index 0000000000..203f9ec1a4 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftRemoveInfoAlertController.swift @@ -0,0 +1,347 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import AppBundle +import Markdown +import ChatMessagePaymentAlertController +import ActivityIndicator +import MultilineTextWithEntitiesComponent +import TelegramStringFormatting +import TextFormat + +private final class GiftRemoveInfoAlertContentNode: AlertContentNode { + private let context: AccountContext + private let strings: PresentationStrings + private var presentationTheme: PresentationTheme + private let title: String + private let text: String + private let gift: StarGift.UniqueGift + private let peers: [EnginePeer.Id: EnginePeer] + + private let titleNode: ASTextNode + private let textNode: ASTextNode + private let infoBackgroundNode: ASDisplayNode + private let infoView = ComponentView() + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private var activityIndicator: ActivityIndicator? + + private var validLayout: CGSize? + + var inProgress = false { + didSet { + if let size = self.validLayout { + let _ = self.updateLayout(size: size, transition: .immediate) + } + } + } + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init( + context: AccountContext, + theme: AlertControllerTheme, + ptheme: PresentationTheme, + strings: PresentationStrings, + gift: StarGift.UniqueGift, + peers: [EnginePeer.Id: EnginePeer], + title: String, + text: String, + actions: [TextAlertAction] + ) { + self.context = context + self.strings = strings + self.presentationTheme = ptheme + self.title = title + self.text = text + self.gift = gift + self.peers = peers + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 0 + + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 0 + + self.infoBackgroundNode = ASDisplayNode() + self.infoBackgroundNode.backgroundColor = ptheme.overallDarkAppearance ? ptheme.list.itemModalBlocksBackgroundColor : ptheme.list.itemInputField.backgroundColor + self.infoBackgroundNode.cornerRadius = 10.0 + self.infoBackgroundNode.displaysAsynchronously = false + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + + self.addSubnode(self.infoBackgroundNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.updateTheme(theme) + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor) + self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), + linkAttribute: { url in + return ("URL", url) + } + ), textAlignment: .center) + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width, 310.0) + + let strings = self.strings + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) + + let titleSize = self.titleNode.measure(CGSize(width: size.width - 32.0, height: size.height)) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) + origin.y += titleSize.height + 5.0 + + let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) + origin.y += textSize.height + 10.0 + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + let contentWidth = max(size.width, minActionsWidth) + + let actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + + var infoSize: CGSize = .zero + for attribute in self.gift.attributes { + if case let .originalInfo(senderPeerId, recipientPeerId, date, text, entities) = attribute { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + let tableFont = Font.regular(13.0) + let tableBoldFont = Font.semibold(13.0) + let tableItalicFont = Font.italic(13.0) + let tableBoldItalicFont = Font.semiboldItalic(13.0) + let tableMonospaceFont = Font.monospace(13.0) + + let tableTextColor = self.presentationTheme.list.itemPrimaryTextColor + let tableLinkColor = self.presentationTheme.list.itemAccentColor + + let senderName = senderPeerId.flatMap { self.peers[$0]?.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder) } + let recipientName = self.peers[recipientPeerId]?.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder) ?? "" + + let dateString = stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: presentationData.dateTimeFormat, withTime: false) + let value: NSAttributedString + if let text { + let attributedText = stringWithAppliedEntities(text, entities: entities ?? [], baseColor: tableTextColor, linkColor: tableLinkColor, baseFont: tableFont, linkFont: tableFont, boldFont: tableBoldFont, italicFont: tableItalicFont, boldItalicFont: tableBoldItalicFont, fixedFont: tableMonospaceFont, blockQuoteFont: tableFont, message: nil) + + let format = senderName != nil ? presentationData.strings.Gift_Unique_OriginalInfoSenderWithText(senderName!, recipientName, dateString, "") : presentationData.strings.Gift_Unique_OriginalInfoWithText(recipientName, dateString, "") + let string = NSMutableAttributedString(string: format.string, font: tableFont, textColor: tableTextColor) + string.replaceCharacters(in: format.ranges[format.ranges.count - 1].range, with: attributedText) + if let senderPeerId { + string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: senderPeerId, mention: ""), range: format.ranges[0].range) + string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[1].range) + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: recipientPeerId, mention: ""), range: format.ranges[1].range) + } else { + string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: recipientPeerId, mention: ""), range: format.ranges[0].range) + } + value = string + } else { + let format = senderName != nil ? presentationData.strings.Gift_Unique_OriginalInfoSender(senderName!, recipientName, dateString) : presentationData.strings.Gift_Unique_OriginalInfo(recipientName, dateString) + let string = NSMutableAttributedString(string: format.string, font: tableFont, textColor: tableTextColor) + if let senderPeerId { + string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: senderPeerId, mention: ""), range: format.ranges[0].range) + string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[1].range) + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: recipientPeerId, mention: ""), range: format.ranges[1].range) + } else { + string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: recipientPeerId, mention: ""), range: format.ranges[0].range) + } + + value = string + } + + infoSize = self.infoView.update( + transition: .immediate, + component: AnyComponent( + MultilineTextWithEntitiesComponent( + context: self.context, + animationCache: self.context.animationCache, + animationRenderer: self.context.animationRenderer, + placeholderColor: self.presentationTheme.list.mediaPlaceholderColor, + text: .plain(value), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + handleSpoilers: true + ) + ), + environment: {}, + containerSize: CGSize(width: contentWidth - 32.0, height: size.height) + ) + let infoFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - infoSize.width) / 2.0), y: titleSize.height + textSize.height + 54.0), size: infoSize) + if let view = self.infoView.view { + if view.superview == nil { + self.view.addSubview(view) + } + view.frame = infoFrame + } + self.infoBackgroundNode.frame = infoFrame.insetBy(dx: -12.0, dy: -12.0) + + break + } + } + + let resultSize = CGSize(width: contentWidth, height: titleSize.height + textSize.height + infoSize.height + actionsHeight + 46.0 + insets.top + insets.bottom) + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + do { + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + do { + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + do { + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + if self.inProgress { + let activityIndicator: ActivityIndicator + if let current = self.activityIndicator { + activityIndicator = current + } else { + activityIndicator = ActivityIndicator(type: .custom(self.presentationTheme.list.freeInputField.controlColor, 18.0, 1.5, false)) + self.addSubnode(activityIndicator) + } + + if let actionNode = self.actionNodes.first { + actionNode.isHidden = true + + let indicatorSize = CGSize(width: 22.0, height: 22.0) + transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: actionNode.frame.minX + floor((actionNode.frame.width - indicatorSize.width) / 2.0), y: actionNode.frame.minY + floor((actionNode.frame.height - indicatorSize.height) / 2.0)), size: indicatorSize)) + } + } + + return resultSize + } +} + +public func giftRemoveInfoAlertController( + context: AccountContext, + gift: StarGift.UniqueGift, + peers: [EnginePeer.Id: EnginePeer], + removeInfoStars: Int64, + navigationController: NavigationController?, + commit: @escaping () -> Void +) -> AlertController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings + + //TODO:localize + let title = "Remove Description" + let text = "Do you want to permanently remove this description from your gift?" + let buttonText = "Remove for $ \(presentationStringsFormattedNumber(Int32(clamping: removeInfoStars), presentationData.dateTimeFormat.groupingSeparator))" + + var contentNode: GiftRemoveInfoAlertContentNode? + var dismissImpl: ((Bool) -> Void)? + let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: buttonText, action: { [weak contentNode] in + contentNode?.inProgress = true + commit() + }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?(true) + })] + + contentNode = GiftRemoveInfoAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peers: peers, title: title, text: text, actions: actions) + + let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode!, navigationController: navigationController, chatPeerId: context.account.peerId, showBalance: removeInfoStars > 0) + dismissImpl = { [weak controller] animated in + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index ce40d564ce..d47ffba54a 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -664,6 +664,22 @@ private final class GiftViewSheetContent: CombinedComponent { controller.push(introController) } + func openRemoveInfo() { + guard let controller = self.getController(), let gift = self.subject.arguments?.gift, case let .unique(uniqueGift) = gift else { + return + } + //TODO:release + let removeInfoController = giftRemoveInfoAlertController( + context: self.context, + gift: uniqueGift, + peers: self.peerMap, + removeInfoStars: 1000, + navigationController: controller.navigationController as? NavigationController, + commit: {} + ) + controller.present(removeInfoController, in: .window(.root)) + } + private var isOpeningValue = false func openValue() { guard let controller = self.getController(), let gift = self.subject.arguments?.gift, case let .unique(uniqueGift) = gift, !self.isOpeningValue else { @@ -3406,7 +3422,7 @@ private final class GiftViewSheetContent: CombinedComponent { case let .originalInfo(senderPeerId, recipientPeerId, date, text, entities): id = "originalInfo" title = nil - hasBackground = true + hasBackground = false let tableFont = Font.regular(13.0) let tableBoldFont = Font.semibold(13.0) @@ -3469,7 +3485,7 @@ private final class GiftViewSheetContent: CombinedComponent { animationRenderer: component.context.animationRenderer, placeholderColor: theme.list.mediaPlaceholderColor, text: .plain(value), - horizontalAlignment: .center, + horizontalAlignment: .left, maximumNumberOfLines: 0, insets: id == "originalInfo" ? UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0) : .zero, highlightColor: tableLinkColor.withAlphaComponent(0.1), @@ -3508,8 +3524,25 @@ private final class GiftViewSheetContent: CombinedComponent { ).tagged(tag)) )) } + + var itemAlignment: HStackAlignment = .left + var itemSpacing: CGFloat = 4.0 + if id == "originalInfo" { + items.append(AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Delete", tintColor: tableLinkColor)), + action: { [weak state] in + state?.openRemoveInfo() + } + )) + )) + itemAlignment = .alternatingLeftRight + itemSpacing = 16.0 + } + var itemComponent = AnyComponent( - HStack(items, spacing: 4.0) + HStack(items, spacing: itemSpacing, alignment: itemAlignment) ) if !otherValuesAndPercentages.isEmpty { diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index e4d47d7a70..efecd0f196 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -360,9 +360,21 @@ public final class AccountContextImpl: AccountContext { |> deliverOnMainQueue).start(next: { value in let _ = currentAppConfiguration.swap(value) - if let data = appConfiguration.data, data["ios_killswitch_contact_diffing"] != nil { + guard let data = appConfiguration.data else { + return + } + + if data["ios_killswitch_contact_diffing"] != nil { sharedDisableDeviceContactDataDiffing = true } + + if let url = data["ios_update_url"] as? String, !url.isEmpty { + let _ = (sharedContext.accountManager.transaction { transaction -> Void in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.updateSettings, { _ in + return PreferencesEntry(UpdateSettings(url: url)) + }) + }).start() + } }) let langCode = sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 4c836d070c..1bee710d4c 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -2941,19 +2941,27 @@ private func extractAccountManagerState(records: AccountRecordsView take(1) + |> mapToSignal { sharedContext in + return sharedContext.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.updateSettings]) + |> map { sharedData in + return (sharedContext, sharedData.entries[ApplicationSpecificSharedDataKeys.updateSettings]?.get(UpdateSettings.self)?.url) + } + } + |> deliverOnMainQueue).start(next: { sharedContext, urlString in + guard let url = urlString.flatMap({ URL(string: $0) }) else { + return + } self.currentCheckForUpdatesDisposable.set((downloadHTTPData(url: url) - |> deliverOnMainQueue).start(next: { [weak self] data in - guard let strongSelf = self else { - return - } + |> deliverOnMainQueue).start(next: { data in guard let json = try? JSONSerialization.jsonObject(with: data, options: []) else { return } @@ -2963,27 +2971,23 @@ private func extractAccountManagerState(records: AccountRecordsView take(1) - |> deliverOnMainQueue).start(next: { sharedContext in - let presentationData = sharedContext.sharedContext.currentPresentationData.with { $0 } - sharedContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: "A new build is available", actions: [ - TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), - TextAlertAction(type: .defaultAction, title: "Show", action: { - sharedContext.sharedContext.applicationBindings.openUrl(releaseNotesUrl) - }) - ]), on: .root, blockInteraction: false, completion: {}) - }) + let presentationData = sharedContext.sharedContext.currentPresentationData.with { $0 } + sharedContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: "A new build is available", actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: "Show", action: { + sharedContext.sharedContext.applicationBindings.openUrl(releaseNotesUrl) + }) + ]), on: .root, blockInteraction: false, completion: {}) } })) - } + }) } #endif } @@ -3193,3 +3197,15 @@ private func getMemoryConsumption() -> Int { } return Int(info.phys_footprint) } + +final class UpdateSettings: Codable, Equatable { + let url: String? + + init(url: String?) { + self.url = url + } + + static func ==(lhs: UpdateSettings, rhs: UpdateSettings) -> Bool { + return lhs.url == rhs.url + } +} diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index f10c89be0f..51296e37f1 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -42,6 +42,7 @@ private enum ApplicationSpecificSharedDataKeyValues: Int32 { case translationSettings = 18 case drawingSettings = 19 case mediaDisplaySettings = 20 + case updateSettings = 21 } public struct ApplicationSpecificSharedDataKeys { @@ -66,6 +67,7 @@ public struct ApplicationSpecificSharedDataKeys { public static let translationSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.translationSettings.rawValue) public static let drawingSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.drawingSettings.rawValue) public static let mediaDisplaySettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.mediaDisplaySettings.rawValue) + public static let updateSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.updateSettings.rawValue) } private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 {