Various improvements

This commit is contained in:
Ilya Laktyushin 2025-09-09 20:39:17 +04:00
parent 3e483aad40
commit 94ee23649d
15 changed files with 387 additions and 56 deletions

View file

@ -15017,4 +15017,4 @@ Sorry for the inconvenience.";
"Stars.Purchase.RemoveOriginalDetailsStarGiftInfo" = "Buy Stars to remove original details of your gift.";
"Notification.StarGift.TitleTo" = "Gift for %@";

View file

@ -703,6 +703,7 @@ public enum PeerInfoControllerMode {
case recommendedChannels
case myProfile
case gifts
case upgradableGifts
case myProfileGifts
case groupsInCommon
case monoforum(EnginePeer.Id)

View file

@ -60,6 +60,7 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
}
public let content: AnyComponent<ChildEnvironmentType>
public let headerContent: AnyComponent<Empty>?
public let backgroundColor: BackgroundColor
public let followContentSizeChanges: Bool
public let clipsContent: Bool
@ -73,6 +74,7 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
public init(
content: AnyComponent<ChildEnvironmentType>,
headerContent: AnyComponent<Empty>? = nil,
backgroundColor: BackgroundColor,
followContentSizeChanges: Bool = false,
clipsContent: Bool = false,
@ -85,6 +87,7 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
willDismiss: @escaping () -> Void = {}
) {
self.content = content
self.headerContent = headerContent
self.backgroundColor = backgroundColor
self.followContentSizeChanges = followContentSizeChanges
self.clipsContent = clipsContent
@ -101,6 +104,9 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
if lhs.content != rhs.content {
return false
}
if lhs.headerContent != rhs.headerContent {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
@ -159,6 +165,7 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
private let backgroundView: UIView
private var effectView: UIVisualEffectView?
private let contentView: ComponentView<ChildEnvironmentType>
private var headerView: ComponentView<Empty>?
private var isAnimatingOut: Bool = false
private var previousIsDisplaying: Bool = false
@ -272,10 +279,12 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let headerView = self.headerView?.view, headerView.bounds.contains(self.convert(point, to: headerView)) {
return super.hitTest(point, with: event)
}
if !self.backgroundView.bounds.contains(self.convert(point, to: self.backgroundView)) {
return self.dimView
}
return super.hitTest(point, with: event)
}
@ -288,6 +297,10 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
transition.animateView(allowUserInteraction: true, {
self.scrollView.center = targetPosition
})
if let headerContent = self.headerView {
headerContent.view?.layer.animateAlpha(from: 0.1, to: 0.0, duration: 0.15)
}
}
private func animateOut(initialVelocity: CGFloat? = nil, completion: @escaping () -> Void) {
@ -300,6 +313,10 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
self.isUserInteractionEnabled = false
self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
if let headerContent = self.headerView {
headerContent.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
guard let contentView = self.contentView.view else {
return
}
@ -404,6 +421,33 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
}
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil)
if let headerContent = component.headerContent {
let headerView: ComponentView<Empty>
if let current = self.headerView {
headerView = current
} else {
headerView = ComponentView()
self.headerView = headerView
}
let headerSize = headerView.update(
transition: transition,
component: headerContent,
environment: {},
containerSize: CGSize(width: contentSize.width, height: 44.0)
)
let headerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - headerSize.width) / 2.0), y: self.backgroundView.frame.minY - headerSize.height - 10.0), size: headerSize)
if let headerView = headerView.view {
if headerView.superview == nil {
self.scrollView.addSubview(headerView)
}
transition.setFrame(view: headerView, frame: headerFrame)
}
} else if let headerView = self.headerView {
self.headerView = nil
headerView.view?.removeFromSuperview()
}
let previousContentSize = self.scrollView.contentSize
let updateContentSize = {
self.scrollView.contentSize = contentSize

View file

@ -558,7 +558,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
isStarGift = true
var authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
let isSelfGift = item.message.id.peerId == item.context.account.peerId
let isChannelGift = item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel || channelPeerId != nil
if isSelfGift {
@ -569,8 +569,12 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
} else {
if let senderPeerId, let name = item.message.peers[senderPeerId].flatMap(EnginePeer.init)?.compactDisplayTitle {
authorName = name
title = item.presentationData.strings.Notification_StarGift_Title(authorName).string
} else if !incoming, let name = item.message.peers[item.message.id.peerId].flatMap(EnginePeer.init)?.compactDisplayTitle {
title = item.presentationData.strings.Notification_StarGift_TitleTo(name).string
} else {
title = item.presentationData.strings.Gift_View_Unknown_Title
}
title = item.presentationData.strings.Notification_StarGift_Title(authorName).string
}
}
if let giftText, !giftText.isEmpty {
@ -647,7 +651,11 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
buttonTitle = item.presentationData.strings.Notification_StarGift_Unpack
buttonIcon = "GiftUnpack"
} else {
buttonTitle = item.presentationData.strings.Notification_StarGift_View
if isPrepaidUpgrade && !incoming {
buttonTitle = ""
} else {
buttonTitle = item.presentationData.strings.Notification_StarGift_View
}
}
}
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _, _, _):

View file

@ -41,6 +41,7 @@ public final class EmojiStatusComponent: Component {
public enum SizeType {
case compact
case large
case smaller
}
public enum Content: Equatable {
@ -357,7 +358,7 @@ public final class EmojiStatusComponent: Component {
case let .verified(fillColor, foregroundColor, sizeType):
let imageNamePrefix: String
switch sizeType {
case .compact:
case .compact, .smaller:
imageNamePrefix = "Chat List/PeerVerifiedIcon"
case .large:
imageNamePrefix = "Peer Info/VerifiedIcon"

View file

@ -506,14 +506,27 @@ final class GiftOptionsScreenComponent: Component {
}
isSoldOut = true
} else if let _ = gift.availability {
let text: String
if let perUserLimit = gift.perUserLimit {
text = "\(perUserLimit.remains) left"
} else {
text = environment.strings.Gift_Options_Gift_Limited
}
ribbon = GiftItemComponent.Ribbon(
text: environment.strings.Gift_Options_Gift_Limited,
text: text,
color: .blue
)
}
if !isSoldOut && gift.flags.contains(.requiresPremium) {
let text: String
if component.context.isPremium, let perUserLimit = gift.perUserLimit {
//TODO:localize
text = "\(perUserLimit.remains) left"
} else {
text = environment.strings.Gift_Options_Gift_Premium
}
ribbon = GiftItemComponent.Ribbon(
text: environment.strings.Gift_Options_Gift_Premium,
text: text,
color: .orange
)
outline = .orange

View file

@ -457,6 +457,35 @@ final class GiftSetupScreenComponent: Component {
controllers.append(chatController)
}
navigationController.setViewControllers(controllers, animated: true)
if case let .starGift(starGift, _) = component.subject, let perUserLimit = starGift.perUserLimit {
Queue.mainQueue().after(0.5) {
//TODO:localize
let remains = max(0, perUserLimit.remains - 1)
let text: String
if remains == 0 {
text = "You've reached your limit on this gift."
} else {
text = "You can send **\(remains)** more."
}
let tooltipController = UndoOverlayController(
presentationData: presentationData,
content: .sticker(
context: context,
file: starGift.file,
loop: true,
title: "Gift Sent!",
text: text,
undoText: nil,
customAction: nil
),
position: .top,
action: { _ in return true }
)
(navigationController.viewControllers.last as? ViewController)?.present(tooltipController, in: .current)
}
}
}
if let completion {

View file

@ -138,7 +138,7 @@ private final class GiftTransferAlertContentNode: AlertContentNode {
return ("URL", url)
}
), textAlignment: .center)
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor)
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor.withAlphaComponent(0.9))
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {

View file

@ -537,7 +537,7 @@ private final class GiftViewSheetContent: CombinedComponent {
action: { [weak navigationController] action in
if case .undo = action, let navigationController, let giftsPeerId {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: giftsPeerId))
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in
guard let peer, let navigationController else {
return
}
@ -564,7 +564,7 @@ private final class GiftViewSheetContent: CombinedComponent {
}
func convertToStars() {
guard let controller = self.getController() as? GiftViewScreen, let starsContext = context.starsContext, let arguments = self.subject.arguments, let reference = arguments.reference, let fromPeerName = arguments.fromPeerName, let convertStars = arguments.convertStars, let navigationController = controller.navigationController as? NavigationController else {
guard let controller = self.getController() as? GiftViewScreen, let starsContext = context.starsContext, let arguments = self.subject.arguments, let reference = arguments.reference, let fromPeerName = arguments.fromPeerCompactName, let convertStars = arguments.convertStars, let navigationController = controller.navigationController as? NavigationController else {
return
}
@ -2036,7 +2036,7 @@ private final class GiftViewSheetContent: CombinedComponent {
}
func commitPrepaidUpgrade() {
guard let arguments = self.subject.arguments, let peerId = arguments.peerId, let prepaidUpgradeHash = arguments.prepaidUpgradeHash, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else {
guard let arguments = self.subject.arguments, let controller = self.getController() as? GiftViewScreen, let peerId = arguments.peerId, let prepaidUpgradeHash = arguments.prepaidUpgradeHash, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else {
return
}
guard case let .generic(gift) = arguments.gift else {
@ -2046,6 +2046,7 @@ private final class GiftViewSheetContent: CombinedComponent {
return
}
let context = self.context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let proceed: () -> Void = { [weak self, weak starsContext] in
guard let self else {
return
@ -2068,7 +2069,7 @@ private final class GiftViewSheetContent: CombinedComponent {
}
self.upgradeDisposable = (signal
|> deliverOnMainQueue).start(next: { [weak self, weak starsContext] result in
|> deliverOnMainQueue).start(next: { [weak self, weak controller, weak starsContext] result in
guard let self else {
return
}
@ -2076,12 +2077,54 @@ private final class GiftViewSheetContent: CombinedComponent {
starsContext?.load(force: true)
}
let navigationController = controller?.navigationController as? NavigationController
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
|> deliverOnMainQueue).start(next: { [weak self, weak navigationController] peer in
guard let self, let peer else {
return
}
self.openPeer(peer, gifts: false, dismiss: true)
Queue.mainQueue().after(0.5) {
if let lastController = navigationController?.viewControllers.last as? ViewController {
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .sticker(
context: context,
file: gift.file,
loop: false,
title: nil,
text: "Upgrade sent!",
undoText: "Gift More",
customAction: nil
),
elevatedLayout: !(lastController is ChatController),
action: { [weak navigationController] action in
if case .undo = action, let navigationController {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in
guard let peer, let navigationController else {
return
}
if let controller = context.sharedContext.makePeerInfoController(
context: context,
updatedPresentationData: nil,
peer: peer._asPeer(),
mode: .upgradableGifts,
avatarInitiallyExpanded: false,
fromChat: false,
requestsContext: nil
) {
navigationController.pushViewController(controller, animated: true)
}
})
}
return true
}
)
lastController.present(resultController, in: .current)
}
}
})
}, error: { _ in
@ -2092,8 +2135,8 @@ private final class GiftViewSheetContent: CombinedComponent {
let _ = (self.optionsPromise.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] options in
guard let self, let controller = self.getController() else {
|> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] options in
guard let self, let controller else {
return
}
let purchaseController = self.context.sharedContext.makeStarsPurchaseScreen(
@ -4322,7 +4365,8 @@ private final class GiftViewSheetContent: CombinedComponent {
buttonTitleItems.append(AnyComponentWithIdentity(id: "timer", component: AnyComponent(AnimatedTextComponent(
font: Font.with(size: 11.0, weight: .medium, traits: .monospacedNumbers),
color: environment.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7),
items: buttonAnimatedTitleItems
items: buttonAnimatedTitleItems,
noDelay: true
))))
} else {
buttonTitleItems.append(AnyComponentWithIdentity(id: "static_label", component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))))
@ -4347,7 +4391,7 @@ private final class GiftViewSheetContent: CombinedComponent {
availableSize: buttonSize,
transition: .spring(duration: 0.2)
)
} else if upgraded, let upgradeMessageIdId = subject.arguments?.upgradeMessageId, let originalMessageId = subject.arguments?.messageId {
} else if upgraded, let arguments = subject.arguments, let upgradeMessageIdId = arguments.upgradeMessageId, let originalMessageId = arguments.messageId, !arguments.upgradeSeparate {
let upgradeMessageId = MessageId(peerId: originalMessageId.peerId, namespace: originalMessageId.namespace, id: upgradeMessageIdId)
let buttonTitle = strings.Gift_View_ViewUpgraded
buttonChild = button.update(
@ -4564,13 +4608,13 @@ private final class GiftViewSheetContent: CombinedComponent {
originY += buttonChild.size.height
originY += 7.0
if showUpgradePreview {
if showUpgradePreview, let _ = state.nextUpgradePrice {
originY += 20.0
if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme {
state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: theme.actionSheet.controlAccentColor)!, theme)
}
//TODO:localize
let attributedString = NSMutableAttributedString(string: "See how price will decrease >", font: Font.regular(13.0), textColor: theme.actionSheet.controlAccentColor)
if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 {
attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string))
@ -4642,6 +4686,43 @@ final class GiftViewSheetComponent: CombinedComponent {
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
var headerContent: AnyComponent<Empty>?
if let arguments = context.component.subject.arguments, case .unique = arguments.gift, let fromPeerId = arguments.fromPeerId, let fromPeerName = arguments.fromPeerName, arguments.fromPeerId != context.component.context.account.peerId {
let dateString = stringForMediumDate(timestamp: arguments.date, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, withTime: false)
let rawString = "**\(fromPeerName)** sent you this gift on **\(dateString)**."
let attributedString = parseMarkdownIntoAttributedString(rawString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: .white), bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: .white), link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: .white), linkAttribute: { _ in return nil }))
let context = context.component.context
headerContent = AnyComponent(
PlainButtonComponent(content: AnyComponent(HeaderContentComponent(attributedText: attributedString)), action: {
if let controller = controller(), let navigationController = controller.navigationController as? NavigationController {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: fromPeerId))
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in
guard let peer, let navigationController else {
return
}
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
navigationController: navigationController,
chatController: nil,
context: context,
chatLocation: .peer(peer),
subject: nil,
botStart: nil,
updateTextInputState: nil,
keepStack: .always,
useExisting: true,
purposefulAction: nil,
scrollToEndIfExists: false,
activateMessageSearch: nil,
animated: true
))
})
}
})
)
}
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(GiftViewSheetContent(
@ -4650,10 +4731,12 @@ final class GiftViewSheetComponent: CombinedComponent {
animateOut: animateOut,
getController: controller
)),
headerContent: headerContent,
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
clipsContent: true,
hasDimView: false,
autoAnimateOut: false,
externalState: sheetExternalState,
animateOut: animateOut,
onPan: {
@ -4740,7 +4823,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
case upgradePreview([StarGift.UniqueGift.Attribute], String)
case wearPreview(StarGift.UniqueGift)
var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellAmounts: [CurrencyAmount]?, canExportDate: Int32?, upgradeMessageId: Int32?, canTransferDate: Int32?, canResaleDate: Int32?, prepaidUpgradeHash: String?, upgradeSeparate: Bool, dropOriginalDetailsStars: Int64?)? {
var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, fromPeerCompactName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellAmounts: [CurrencyAmount]?, canExportDate: Int32?, upgradeMessageId: Int32?, canTransferDate: Int32?, canResaleDate: Int32?, prepaidUpgradeHash: String?, upgradeSeparate: Bool, dropOriginalDetailsStars: Int64?)? {
switch self {
case let .message(message):
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction {
@ -4754,7 +4837,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
} else {
reference = .message(messageId: message.id)
}
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil, prepaidUpgradeHash, upgradeSeparate, nil)
return (message.id.peerId, senderId ?? message.author?.id, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil, prepaidUpgradeHash, upgradeSeparate, nil)
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars):
var reference: StarGiftReference
if let peerId, let savedId {
@ -4779,13 +4862,13 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if case let .unique(uniqueGift) = gift {
resellAmounts = uniqueGift.resellAmounts
}
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellAmounts, canExportDate, nil, canTransferDate, canResaleDate, nil, false, dropOriginalDetailsStars)
return (message.id.peerId, senderId ?? message.author?.id, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellAmounts, canExportDate, nil, canTransferDate, canResaleDate, nil, false, dropOriginalDetailsStars)
default:
return nil
}
}
case let .uniqueGift(gift, _), let .wearPreview(gift):
return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellAmounts, nil, nil, nil, nil, nil, false, nil)
return (nil, nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellAmounts, nil, nil, nil, nil, nil, false, nil)
case let .profileGift(peerId, gift):
var messageId: EngineMessage.Id?
if case let .message(messageIdValue) = gift.reference {
@ -4795,7 +4878,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if case let .unique(uniqueGift) = gift.gift {
resellAmounts = uniqueGift.resellAmounts
}
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellAmounts, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate, gift.prepaidUpgradeHash, gift.upgradeSeparate, gift.dropOriginalDetailsStars)
return (peerId, gift.fromPeer?.id, gift.fromPeer?.debugDisplayTitle, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellAmounts, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate, gift.prepaidUpgradeHash, gift.upgradeSeparate, gift.dropOriginalDetailsStars)
case .soldOutGift:
return nil
case .upgradePreview:
@ -5240,6 +5323,79 @@ private final class PeerCellComponent: Component {
}
}
final class HeaderContentComponent: Component {
let attributedText: NSAttributedString
init(
attributedText: NSAttributedString
) {
self.attributedText = attributedText
}
static func ==(lhs: HeaderContentComponent, rhs: HeaderContentComponent) -> Bool {
if lhs.attributedText != rhs.attributedText {
return false
}
return true
}
final class View: UIView {
private var component: HeaderContentComponent?
private let backgroundView: BlurredBackgroundView
private let title = ComponentView<Empty>()
override init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: UIColor.black.withAlphaComponent(0.2))
super.init(frame: frame)
self.addSubview(self.backgroundView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: HeaderContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(text: .plain(component.attributedText), horizontalAlignment: .center)
),
environment: {},
containerSize: availableSize
)
let padding: CGFloat = 10.0
let size = CGSize(width: titleSize.width + padding * 2.0, height: 19.0)
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0) - UIScreenPixel), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
self.backgroundView.update(size: size, cornerRadius: size.height / 2.0, transition: transition.containedViewLayoutTransition)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: size))
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class ButtonContentComponent: Component {
let context: AccountContext
let text: String

View file

@ -70,6 +70,7 @@ final class TableComponent: CombinedComponent {
}
final class State: ComponentState {
var cachedLeftColumnImage: (UIImage, PresentationTheme)?
var cachedBorderImage: (UIImage, PresentationTheme)?
}
@ -78,7 +79,7 @@ final class TableComponent: CombinedComponent {
}
public static var body: Body {
let leftColumnBackground = Child(Rectangle.self)
let leftColumnBackground = Child(Image.self)
let lastBackground = Child(Rectangle.self)
let verticalBorder = Child(Rectangle.self)
let titleChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
@ -194,25 +195,39 @@ final class TableComponent: CombinedComponent {
)
}
let borderRadius: CGFloat = 10.0
let leftColumnImage: UIImage
if let (currentImage, theme) = context.state.cachedLeftColumnImage, theme === context.component.theme {
leftColumnImage = currentImage
} else {
leftColumnImage = generateImage(CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in
let bounds = CGRect(origin: .zero, size: CGSize(width: size.width + borderRadius, height: size.height))
context.clear(bounds)
let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil)
context.setFillColor(secondaryBackgroundColor.cgColor)
context.addPath(path)
context.fillPath()
})!.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10)
context.state.cachedLeftColumnImage = (leftColumnImage, context.component.theme)
}
let leftColumnBackground = leftColumnBackground.update(
component: Rectangle(color: secondaryBackgroundColor),
component: Image(image: leftColumnImage),
availableSize: CGSize(width: leftColumnWidth, height: innerTotalHeight),
transition: context.transition
)
context.add(
leftColumnBackground
.position(CGPoint(x: leftColumnWidth / 2.0, y: innerTotalHeight / 2.0))
context.add(leftColumnBackground
.position(CGPoint(x: leftColumnWidth / 2.0, y: innerTotalHeight / 2.0))
)
let borderImage: UIImage
if let (currentImage, theme) = context.state.cachedBorderImage, theme === context.component.theme {
borderImage = currentImage
} else {
let borderRadius: CGFloat = 10.0
borderImage = generateImage(CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in
let bounds = CGRect(origin: .zero, size: size)
context.setFillColor(backgroundColor.cgColor)
context.fill(bounds)
context.clear(bounds)
let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil)
context.setBlendMode(.clear)

View file

@ -180,7 +180,7 @@ final class StarsBalanceComponent: Component {
let formattedLabel: String
switch component.currency {
case .ton:
formattedLabel = formatTonAmountText(component.count.value, dateTimeFormat: component.dateTimeFormat)
formattedLabel = formatTonAmountText(component.count.value, dateTimeFormat: component.dateTimeFormat, maxDecimalPositions: 3)
case .stars:
formattedLabel = formatStarsAmountText(component.count, dateTimeFormat: component.dateTimeFormat)
}

View file

@ -466,7 +466,7 @@ private final class SheetContent: CombinedComponent {
case .ton:
if let value = state.amount?.value, value > 0 {
let tonValue = Int64(Float(value) * Float(resaleConfiguration.starGiftCommissionTonPermille) / 1000.0)
let tonString = formatTonAmountText(tonValue, dateTimeFormat: environment.dateTimeFormat, maxDecimalPositions: nil) + " TON"
let tonString = formatTonAmountText(tonValue, dateTimeFormat: environment.dateTimeFormat, maxDecimalPositions: 3) + " TON"
amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_SellGift_AmountInfo(tonString).string, attributes: amountMarkdownAttributes, textAlignment: .natural))
if let tonUsdRate = withdrawConfiguration.tonUsdRate {
@ -1230,7 +1230,13 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
var text = presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum(presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum_Stars(Int32(clamping: minAmount))).string
if case .starGiftResell = self.mode {
text = presentationData.strings.Stars_SellGiftMinAmountToast_Text("\(presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum_Stars(Int32(clamping: minAmount)))").string
switch currency {
case .stars:
text = presentationData.strings.Stars_SellGiftMinAmountToast_Text("\(presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum_Stars(Int32(clamping: minAmount)))").string
case .ton:
let amountString = formatTonAmountText(minAmount, dateTimeFormat: presentationData.dateTimeFormat) + " TON"
text = presentationData.strings.Stars_SellGiftMinAmountToast_Text(amountString).string
}
} else if case let .suggestedPost(mode, _, _, _) = self.mode {
let resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 })
switch currency {
@ -1253,7 +1259,7 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .image(
image: UIImage(bundleImageName: "Premium/Stars/StarLarge")!,
image: currency == .ton ? generateTintedImage(image: UIImage(bundleImageName: "Premium/TonGift"), color: .white)! : UIImage(bundleImageName: "Premium/Stars/StarLarge")!,
title: nil,
text: text,
round: false,
@ -1314,9 +1320,9 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate {
}
case .ton:
let scale: Int64 = 1_000_000_000 // 10 (one nano)
if let dot = text.firstIndex(of: ".") {
if let decimalSeparator = self.dateTimeFormat.decimalSeparator.first, let dot = text.firstIndex(of: decimalSeparator) {
// Slices for the parts on each side of the dot
var wholeSlice = String(text[..<dot])
var wholeSlice = String(text[..<dot])
if wholeSlice.isEmpty {
wholeSlice = "0"
}
@ -1356,7 +1362,7 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
var acceptZero = false
if self.minValue <= 0 {
if case .ton = self.currency, self.minValue < 1_000_000_000 {
acceptZero = true
}
@ -1367,7 +1373,7 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate {
return false
default:
if case .ton = self.currency {
if c == "." {
if let decimalSeparator = self.dateTimeFormat.decimalSeparator.first, c == decimalSeparator {
return false
}
}
@ -1376,7 +1382,7 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate {
}) {
return false
}
if newText.count(where: { $0 == "." }) > 1 {
if let decimalSeparator = self.dateTimeFormat.decimalSeparator.first, newText.count(where: { $0 == decimalSeparator }) > 1 {
return false
}
@ -1390,7 +1396,7 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate {
}
case .ton:
var fixedText = false
if let index = newText.firstIndex(of: ".") {
if let decimalSeparator = self.dateTimeFormat.decimalSeparator.first, let index = newText.firstIndex(of: decimalSeparator) {
let fractionalString = newText[newText.index(after: index)...]
if fractionalString.count > 2 {
newText = String(newText[newText.startIndex ..< newText.index(index, offsetBy: 3)])
@ -1398,7 +1404,16 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate {
}
}
if (newText == "0" && !acceptZero) || (newText.count > 1 && newText.hasPrefix("0") && !newText.hasPrefix("0.")) {
if newText == self.dateTimeFormat.decimalSeparator {
if !acceptZero {
newText.removeFirst()
} else {
newText = "0\(newText)"
}
fixedText = true
}
if (newText == "0" && !acceptZero) || (newText.count > 1 && newText.hasPrefix("0") && !newText.hasPrefix("0\(self.dateTimeFormat.decimalSeparator)")) {
newText.removeFirst()
fixedText = true
}
@ -1416,7 +1431,7 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate {
case .stars:
textField.text = "\(self.maxValue)"
case .ton:
textField.text = "\(formatTonAmountText(self.maxValue, dateTimeFormat: PresentationDateTimeFormat(timeFormat: self.dateTimeFormat.timeFormat, dateFormat: self.dateTimeFormat.dateFormat, dateSeparator: "", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), maxDecimalPositions: nil))"
textField.text = "\(formatTonAmountText(self.maxValue, dateTimeFormat: PresentationDateTimeFormat(timeFormat: self.dateTimeFormat.timeFormat, dateFormat: self.dateTimeFormat.dateFormat, dateSeparator: "", dateSuffix: "", requiresFullYear: false, decimalSeparator: self.dateTimeFormat.decimalSeparator, groupingSeparator: ""), maxDecimalPositions: nil))"
}
self.onTextChanged(text: self.textField.text ?? "")
self.animateError()
@ -1639,7 +1654,7 @@ private final class AmountFieldComponent: Component {
self.tonFormatter = nil
self.textField.delegate = self.starsFormatter
case .ton:
self.textField.keyboardType = .numbersAndPunctuation
self.textField.keyboardType = .decimalPad
if self.tonFormatter == nil {
self.tonFormatter = AmountFieldStarsFormatter(
textField: self.textField,

View file

@ -14,6 +14,7 @@ import AsyncDisplayKit
import StoryContainerScreen
import MultilineTextComponent
import HierarchyTrackingLayer
import EmojiStatusComponent
private func calculateCircleIntersection(center: CGPoint, otherCenter: CGPoint, radius: CGFloat) -> (point1Angle: CGFloat, point2Angle: CGFloat)? {
let distanceVector = CGPoint(x: otherCenter.x - center.x, y: otherCenter.y - center.y)
@ -501,6 +502,7 @@ public final class StoryPeerListItemComponent: Component {
private let indicatorShapeSeenLayer: SimpleShapeLayer
private let indicatorShapeUnseenLayer: SimpleShapeLayer
private let title = ComponentView<Empty>()
private var verifiedIconView: ComponentHostView<Empty>?
private let composeTitle = ComponentView<Empty>()
private var component: StoryPeerListItemComponent?
@ -885,7 +887,59 @@ public final class StoryPeerListItemComponent: Component {
environment: {},
containerSize: CGSize(width: availableSize.width + 12.0, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5) + (effectiveWidth - availableSize.width) * 0.5, y: indicatorFrame.midY + (indicatorFrame.height * 0.5 + 2.0) * effectiveScale), size: titleSize)
var totalTitleWidth = titleSize.width
var currentVerifiedIconContent: EmojiStatusComponent.Content?
if component.peer.isVerified {
currentVerifiedIconContent = .verified(fillColor: component.theme.list.itemCheckColors.fillColor, foregroundColor: component.theme.list.itemCheckColors.foregroundColor, sizeType: .smaller)
}
if let currentVerifiedIconContent {
let verifiedIconView: ComponentHostView<Empty>
if let current = self.verifiedIconView {
verifiedIconView = current
} else {
verifiedIconView = ComponentHostView<Empty>()
verifiedIconView.isUserInteractionEnabled = false
self.verifiedIconView = verifiedIconView
self.button.addSubview(verifiedIconView)
}
let containerSize = CGSize(width: 12.0, height: 12.0)
let verifiedIconComponent = EmojiStatusComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
content: currentVerifiedIconContent,
size: containerSize,
isVisibleForAnimations: component.context.sharedContext.energyUsageSettings.loopEmoji,
action: nil
)
let iconSize = verifiedIconView.update(
transition: .immediate,
component: AnyComponent(verifiedIconComponent),
environment: {},
containerSize: containerSize
)
totalTitleWidth += iconSize.width + 1.0
var titleScale = effectiveScale
if titleScale < 1.0 {
titleScale = min(1.0, titleScale + 0.02)
} else if titleScale > 1.01 {
titleScale = max(1.0, titleScale * 0.96)
}
let verifiedIconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - totalTitleWidth) * 0.5) + (titleSize.width + 1.0) * titleScale + (effectiveWidth - availableSize.width) * 0.5, y: indicatorFrame.midY + (indicatorFrame.height * 0.5 + 2.0 + UIScreenPixel) * effectiveScale), size: iconSize)
titleTransition.setPosition(view: verifiedIconView, position: verifiedIconFrame.center)
verifiedIconView.bounds = CGRect(origin: CGPoint(), size: verifiedIconFrame.size)
titleTransition.setScale(view: verifiedIconView, scale: effectiveScale)
titleTransition.setAlpha(view: verifiedIconView, alpha: component.expandedAlphaFraction)
} else if let verifiedIconView = self.verifiedIconView {
self.verifiedIconView = nil
verifiedIconView.removeFromSuperview()
}
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - totalTitleWidth) * 0.5) + (effectiveWidth - availableSize.width) * 0.5, y: indicatorFrame.midY + (indicatorFrame.height * 0.5 + 2.0) * effectiveScale), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)

View file

@ -1137,7 +1137,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
}
if isInternetUrl {
if parsedUrl.host == "t.me" || parsedUrl.host == "telegram.me" {
if parsedUrl.host == "t.me" || parsedUrl.host == "telegram.me" || parsedUrl.host == "telegram.dog" {
handleInternalUrl(parsedUrl.absoluteString)
} else {
let settings = combineLatest(context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings, ApplicationSpecificSharedDataKeys.presentationPasscodeSettings]), context.sharedContext.accountManager.accessChallengeData())
@ -1159,11 +1159,6 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
return settings
}
// var isCompact = false
// if let metrics = navigationController?.validLayout?.metrics, case .compact = metrics.widthClass {
// isCompact = true
// }
let _ = (settings
|> deliverOnMainQueue).startStandalone(next: { settings in
var isTonSite = false
@ -1216,7 +1211,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
}
if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" {
let nativeHosts = ["t.me", "telegram.me"]
let nativeHosts = ["t.me", "telegram.me", "telegram.dog"]
if let host = parsedUrl.host, nativeHosts.contains(host) {
continueHandling()
} else {

View file

@ -3948,7 +3948,7 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation
forumTopicThread = thread
case .recommendedChannels:
switchToRecommendedChannels = true
case .gifts:
case .gifts, .upgradableGifts:
switchToGifts = true
case .groupsInCommon:
switchToGroupsInCommon = true
@ -3986,7 +3986,7 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation
forumTopicThread = thread
case .myProfile:
isMyProfile = true
case .gifts:
case .gifts, .upgradableGifts:
switchToGifts = true
case .myProfileGifts:
isMyProfile = true