Various improvements

This commit is contained in:
Ilya Laktyushin 2025-09-05 16:11:56 +05:00
parent 59239ca2df
commit ef3512668c
5 changed files with 433 additions and 23 deletions

View file

@ -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<Empty>()
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
}

View file

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

View file

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

View file

@ -2941,19 +2941,27 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
private func maybeCheckForUpdates() {
#if targetEnvironment(simulator)
#else
guard let buildConfig = self.buildConfig, let appCenterId = buildConfig.appCenterId, !appCenterId.isEmpty else {
guard let buildConfig = self.buildConfig, buildConfig.isInternalBuild else {
return
}
let timestamp = CFAbsoluteTimeGetCurrent()
if self.lastCheckForUpdatesTimestamp == nil || self.lastCheckForUpdatesTimestamp! < timestamp - 10.0 * 60.0 {
self.lastCheckForUpdatesTimestamp = timestamp
if let url = URL(string: "https://api.appcenter.ms/v0.1/public/sdk/apps/\(appCenterId)/releases/latest") {
let _ = (self.sharedContextPromise.get()
|> 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<TelegramAcco
guard let versionString = dict["version"] as? String, let version = Int(versionString) else {
return
}
guard let releaseNotesUrl = dict["release_notes_url"] as? String else {
guard let releaseNotesUrl = dict["url"] as? String else {
return
}
guard let currentVersionString = Bundle.main.infoDictionary?["CFBundleVersion"] as? String, let currentVersion = Int(currentVersionString) else {
return
}
if currentVersion < version {
let _ = (strongSelf.sharedContextPromise.get()
|> 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
}
}

View file

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