From 7a89a58930ed7924c2d53ed99cfac40eeae628f2 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 28 Feb 2025 20:09:31 +0400 Subject: [PATCH 1/4] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 6 + .../Sources/ShareControllerNode.swift | 4 +- .../UpdatedAccountPrivacySettings.swift | 3 +- .../Sources/GiftViewScreen.swift | 4 +- .../PeerInfo/PeerInfoCoverComponent/BUILD | 1 + .../Sources/PeerInfoGiftsCoverComponent.swift | 687 ++++++++++++++++++ .../Sources/PeerInfoHeaderNode.swift | 32 +- .../Sources/PeerInfoScreen.swift | 22 +- .../Sources/StarsTransactionScreen.swift | 13 +- .../StarsTransactionsListPanelComponent.swift | 14 +- 10 files changed, 767 insertions(+), 19 deletions(-) create mode 100644 submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 3f867105ee..0ec0e83f61 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13863,6 +13863,12 @@ Sorry for the inconvenience."; "Stars.Intro.Transaction.PaidMessage_1" = "Fee for %@ Message"; "Stars.Intro.Transaction.PaidMessage_any" = "Fee for %@ Messages"; +"Stars.Transaction.TelegramPremium_1" = "Premium for %@ Month"; +"Stars.Transaction.TelegramPremium_any" = "Premium for %@ Months"; + +"Stars.Intro.Transaction.TelegramPremium_1" = "Premium for %@ Month"; +"Stars.Intro.Transaction.TelegramPremium_any" = "Premium for %@ Months"; + "Stars.Purchase.SendMessageInfo" = "Buy Stars to send a message to **%@**."; "Stars.Purchase.SendGroupMessageInfo" = "Buy Stars to send a message in **%@**."; diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index 3c71ca02d1..d50cc584f2 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -1329,9 +1329,11 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate if !self.inputFieldNode.text.isEmpty { count += 1 } + var chargingPeers: [EnginePeer] = [] var totalAmount: StarsAmount = .zero for peer in peers { if let stars = requiresStars[peer.id] { + chargingPeers.append(peer) totalAmount = totalAmount + StarsAmount(value: stars, nanos: 0) } } @@ -1340,7 +1342,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate context: nil, presentationData: self.presentationData, updatedPresentationData: nil, - peers: peers, + peers: chargingPeers, count: count, amount: totalAmount, totalAmount: totalAmount, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift index 81711eea39..e9c74aabed 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift @@ -352,7 +352,8 @@ func _internal_updateGlobalPrivacySettings(account: Account, settings: GlobalPri var noncontactPeersPaidStars: Int64? switch settings.nonContactChatsPrivacy { case .everybody: - break + flags |= 1 << 5 + noncontactPeersPaidStars = 0 case .requirePremium: flags |= 1 << 4 case let .paidMessages(starsAmount): diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 4b7dff105c..0beadb7f97 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -2678,7 +2678,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { } convertToStarsImpl = { [weak self] in - guard let self, let arguments = self.subject.arguments, let reference = arguments.reference, let fromPeerName = arguments.fromPeerName, let convertStars = arguments.convertStars, let navigationController = self.navigationController as? NavigationController else { + guard let self, let starsContext = context.starsContext, let arguments = self.subject.arguments, let reference = arguments.reference, let fromPeerName = arguments.fromPeerName, let convertStars = arguments.convertStars, let navigationController = self.navigationController as? NavigationController else { return } @@ -2725,6 +2725,8 @@ public class GiftViewScreen: ViewControllerComponentContainer { if let navigationController { Queue.mainQueue().after(0.5) { + starsContext.load(force: true) + let text: String if isChannelGift { text = presentationData.strings.Gift_Convert_Success_ChannelText(presentationData.strings.Gift_Convert_Success_ChannelText_Stars(Int32(convertStars))).string diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/BUILD index 36d9e699d3..8181c61dd3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/BUILD @@ -22,6 +22,7 @@ swift_library( "//submodules/Components/ComponentDisplayAdapters", "//submodules/TelegramUI/Components/EmojiTextAttachmentView", "//submodules/Utils/LokiRng", + "//submodules/TextFormat", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift new file mode 100644 index 0000000000..b94da9ed13 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift @@ -0,0 +1,687 @@ +import Foundation +import AsyncDisplayKit +import Display +import ComponentFlow +import ComponentDisplayAdapters +import AnimationCache +import MultiAnimationRenderer +import TelegramCore +import AccountContext +import SwiftSignalKit +import EmojiTextAttachmentView +import LokiRng +import TextFormat + +private final class PatternContentsTarget: MultiAnimationRenderTarget { + private let imageUpdated: (Bool) -> Void + + init(imageUpdated: @escaping (Bool) -> Void) { + self.imageUpdated = imageUpdated + + super.init() + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + override func transitionToContents(_ contents: AnyObject, didLoop: Bool) { + let hadContents = self.contents != nil + self.contents = contents + self.imageUpdated(hadContents) + } +} + +private func windowFunction(t: CGFloat) -> CGFloat { + return bezierPoint(0.6, 0.0, 0.4, 1.0, t) +} + +private func patternScaleValueAt(fraction: CGFloat, t: CGFloat, reverse: Bool) -> CGFloat { + let windowSize: CGFloat = 0.8 + + let effectiveT: CGFloat + let windowStartOffset: CGFloat + let windowEndOffset: CGFloat + if reverse { + effectiveT = 1.0 - t + windowStartOffset = 1.0 + windowEndOffset = -windowSize + } else { + effectiveT = t + windowStartOffset = -windowSize + windowEndOffset = 1.0 + } + + let windowPosition = (1.0 - fraction) * windowStartOffset + fraction * windowEndOffset + let windowT = max(0.0, min(windowSize, effectiveT - windowPosition)) / windowSize + let localT = 1.0 - windowFunction(t: windowT) + + return localT +} + +public final class PeerInfoGiftsCoverComponent: Component { + public let context: AccountContext + public let peerId: EnginePeer.Id + public let giftsContext: ProfileGiftsContext + public let avatarCenter: CGPoint + public let avatarScale: CGFloat + public let defaultHeight: CGFloat + public let avatarTransitionFraction: CGFloat + public let patternTransitionFraction: CGFloat + public let hasButtons: Bool + + public init( + context: AccountContext, + peerId: EnginePeer.Id, + giftsContext: ProfileGiftsContext, + avatarCenter: CGPoint, + avatarScale: CGFloat, + defaultHeight: CGFloat, + avatarTransitionFraction: CGFloat, + patternTransitionFraction: CGFloat, + hasButtons: Bool + ) { + self.context = context + self.peerId = peerId + self.giftsContext = giftsContext + self.avatarCenter = avatarCenter + self.avatarScale = avatarScale + self.defaultHeight = defaultHeight + self.avatarTransitionFraction = avatarTransitionFraction + self.patternTransitionFraction = patternTransitionFraction + self.hasButtons = hasButtons + } + + public static func ==(lhs: PeerInfoGiftsCoverComponent, rhs: PeerInfoGiftsCoverComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peerId != rhs.peerId { + return false + } + if lhs.avatarCenter != rhs.avatarCenter { + return false + } + if lhs.avatarScale != rhs.avatarScale { + return false + } + if lhs.defaultHeight != rhs.defaultHeight { + return false + } + if lhs.avatarTransitionFraction != rhs.avatarTransitionFraction { + return false + } + if lhs.patternTransitionFraction != rhs.patternTransitionFraction { + return false + } + if lhs.hasButtons != rhs.hasButtons { + return false + } + return true + } + + public final class View: UIView { + private let avatarBackgroundPatternContentsLayer: SimpleGradientLayer + private let avatarBackgroundPatternMaskLayer: SimpleLayer + private let avatarBackgroundGradientLayer: SimpleGradientLayer + private let backgroundPatternContainer: UIView + + private var currentSize: CGSize? + private var component: PeerInfoGiftsCoverComponent? + private var state: EmptyComponentState? + + private var giftsDisposable: Disposable? + private var gifts: [ProfileGiftsContext.State.StarGift] = [] + + private var iconLayers: [AnyHashable: GiftIconLayer] = [:] + + private var iconPositions: [PositionGenerator.Position] = [] + private let seed = UInt(Date().timeIntervalSince1970) + + override public init(frame: CGRect) { + self.avatarBackgroundGradientLayer = SimpleGradientLayer() + self.avatarBackgroundGradientLayer.opacity = 0.0 + + self.avatarBackgroundGradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5) + self.avatarBackgroundGradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0) + self.avatarBackgroundGradientLayer.type = .radial + + self.avatarBackgroundPatternContentsLayer = SimpleGradientLayer() + self.avatarBackgroundPatternContentsLayer.startPoint = CGPoint(x: 0.5, y: 0.5) + self.avatarBackgroundPatternContentsLayer.endPoint = CGPoint(x: 1.0, y: 1.0) + self.avatarBackgroundPatternContentsLayer.type = .radial + + self.avatarBackgroundPatternMaskLayer = SimpleLayer() + self.backgroundPatternContainer = UIView() + + super.init(frame: frame) + + self.clipsToBounds = true + + self.avatarBackgroundPatternContentsLayer.mask = self.avatarBackgroundPatternMaskLayer + self.layer.addSublayer(self.avatarBackgroundPatternContentsLayer) + + self.addSubview(self.backgroundPatternContainer) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.giftsDisposable?.dispose() + } + + private var isUpdating = false + func update(component: PeerInfoGiftsCoverComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + self.component = component + self.state = state + + let previousCurrentSize = self.currentSize + self.currentSize = availableSize + + let iconSize = CGSize(width: 32.0, height: 32.0) + + if previousCurrentSize?.width != availableSize.width { + var excludeRects: [CGRect] = [] + excludeRects.append(CGRect(origin: .zero, size: CGSize(width: 50.0, height: 90.0))) + excludeRects.append(CGRect(origin: CGPoint(x: availableSize.width - 105.0, y: 0.0), size: CGSize(width: 105.0, height: 90.0))) + excludeRects.append(CGRect(origin: CGPoint(x: floor((availableSize.width - 390.0) / 2.0), y: 0.0), size: CGSize(width: 390.0, height: 50.0))) + excludeRects.append(CGRect(origin: CGPoint(x: floor((availableSize.width - 280.0) / 2.0), y: component.avatarCenter.y + 56.0), size: CGSize(width: 280.0, height: 65.0))) + if component.hasButtons { + excludeRects.append(CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - 81.0), size: CGSize(width: availableSize.width, height: 81.0))) + } + + let positionGenerator = PositionGenerator( + containerSize: availableSize, + avatarFrame: CGSize(width: 100, height: 100).centered(around: component.avatarCenter), + minDistance: 75.0, + maxDistance: availableSize.width / 2.0, + padding: 16.0, + seed: self.seed, + excludeRects: excludeRects + ) + self.iconPositions = positionGenerator.generatePositions(count: 6, viewSize: iconSize) + } + + if self.giftsDisposable == nil { + self.giftsDisposable = combineLatest( + queue: Queue.mainQueue(), + component.giftsContext.state, + component.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId)) + |> map { peer -> Int64? in + if case let .user(user) = peer, case let .starGift(id, _, _, _, _, _, _, _, _) = user.emojiStatus?.content { + return id + } + return nil + } + |> distinctUntilChanged + ).start(next: { [weak self] state, giftStatusId in + guard let self else { + return + } + + let pinnedGifts = state.gifts.filter { gift in + if gift.pinnedToTop { + if case let .unique(uniqueGift) = gift.gift { + return uniqueGift.id != giftStatusId + } + } + return false + } + self.gifts = pinnedGifts + + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + }) + } + + let avatarPatternFrame = CGSize(width: 380.0, height: floor(component.defaultHeight * 1.0)).centered(around: component.avatarCenter) + transition.setFrame(layer: self.avatarBackgroundPatternContentsLayer, frame: avatarPatternFrame) + + self.avatarBackgroundPatternContentsLayer.colors = [ + UIColor.red.withAlphaComponent(0.6).cgColor, + UIColor.red.withAlphaComponent(0.0).cgColor + ] + + let backgroundPatternContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height), size: CGSize(width: availableSize.width, height: 0.0)) + transition.containedViewLayoutTransition.updateFrameAdditive(view: self.backgroundPatternContainer, frame: backgroundPatternContainerFrame) + transition.setAlpha(view: self.backgroundPatternContainer, alpha: component.patternTransitionFraction) + + var validIds = Set() + var index = 0 + for gift in self.gifts.prefix(6) { + let id: AnyHashable + if case let .unique(uniqueGift) = gift.gift { + id = uniqueGift.slug + } else { + id = index + } + validIds.insert(id) + + let iconPosition = self.iconPositions[index] + let iconLayer: GiftIconLayer + if let current = self.iconLayers[id] { + iconLayer = current + } else { + iconLayer = GiftIconLayer(context: component.context, gift: gift, size: iconSize) + iconLayer.startHovering() + self.iconLayers[id] = iconLayer + self.layer.addSublayer(iconLayer) + + iconLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + iconLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + } + + let zeroPosition = component.avatarCenter + let finalPosition = iconPosition.center + let itemScaleFraction = patternScaleValueAt(fraction: component.avatarTransitionFraction, t: 0.0, reverse: false) + + func interpolateRect(from: CGPoint, to: CGPoint, t: CGFloat) -> CGPoint { + let clampedT = max(0, min(1, t)) + + let interpolatedX = from.x + (to.x - from.x) * clampedT + let interpolatedY = from.y + (to.y - from.y) * clampedT + + return CGPoint( + x: interpolatedX, + y: interpolatedY + ) + } + + let effectivePosition = interpolateRect(from: finalPosition, to: zeroPosition, t: itemScaleFraction) + + transition.setBounds(layer: iconLayer, bounds: CGRect(origin: .zero, size: iconSize)) + transition.setPosition(layer: iconLayer, position: effectivePosition) + transition.setScale(layer: iconLayer, scale: iconPosition.scale) + transition.setAlpha(layer: iconLayer, alpha: 1.0 - itemScaleFraction) + + index += 1 + } + + var removeIds: [AnyHashable] = [] + for (id, layer) in self.iconLayers { + if !validIds.contains(id) { + removeIds.append(id) + layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false) + layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + layer.removeFromSuperlayer() + }) + } + } + for id in removeIds { + self.iconLayers.removeValue(forKey: id) + } + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + + +private class PositionGenerator { + private let containerSize: CGSize + private let avatarFrame: CGRect + private let padding: CGFloat + private let minDistance: CGFloat + private let maxDistance: CGFloat + private let rng: LokiRng + + private let excludeRects: [CGRect] + + struct Position { + let center: CGPoint + let scale: CGFloat + } + + init( + containerSize: CGSize, + avatarFrame: CGRect, + minDistance: CGFloat = 20, + maxDistance: CGFloat = 100, + padding: CGFloat = 10, + seed: UInt = UInt.random(in: 0 ..< 10), + excludeRects: [CGRect] = [] + ) { + self.containerSize = containerSize + self.avatarFrame = avatarFrame + self.minDistance = minDistance + self.maxDistance = maxDistance + self.padding = padding + self.rng = LokiRng(seed0: seed, seed1: 0, seed2: 0) + self.excludeRects = excludeRects + } + + func generatePositions(count: Int, viewSize: CGSize) -> [Position] { + let safeCount = min(max(count, 1), 12) // Ensure between 1 and 12 + var positions: [Position] = [] + + let distanceRanges = calculateDistanceRanges(count: safeCount) + + for i in 0.. (viewSize.width + padding) + } + + let distance = hypot(position.x - self.avatarFrame.center.x, position.y - self.avatarFrame.center.y) + var scale = max(1.0, min(0.55, 1.0 - (distance - 50.0) / 100.0)) + scale = scale * scale + + if isFarEnough { + positions.append(Position(center: position, scale: scale)) + foundPosition = true + break + } + } + + if attempts % 5 == 0 && !foundPosition { + currentMaxDist *= 1.2 + } + } + + if !foundPosition { + if let lastChancePosition = generateSinglePosition( + viewSize: viewSize, + minDist: minDist, + maxDist: maxDist * 2.0, // Try with a much larger distance + rightSide: !isEven + ) { + let distance = hypot(lastChancePosition.x - self.avatarFrame.center.x, + lastChancePosition.y - self.avatarFrame.center.y) + var scale = max(1.0, min(0.55, 1.0 - (distance - 50.0) / 100.0)) + scale = scale * scale + + positions.append(Position(center: lastChancePosition, scale: scale)) + } else { + // If all else fails, create a position with default values to ensure we don't return fewer positions than requested + let defaultX = self.avatarFrame.center.x + (isEven ? -1 : 1) * (minDist + CGFloat(i * 20)) + let defaultY = self.avatarFrame.center.y + CGFloat(i * 15) + let defaultPosition = CGPoint(x: defaultX, y: defaultY) + + // Use a smaller scale for these fallback positions + positions.append(Position(center: defaultPosition, scale: 0.5)) + } + } + } + + return positions + } + + private func calculateDistanceRanges(count: Int) -> [(CGFloat, CGFloat)] { + var ranges: [(CGFloat, CGFloat)] = [] + + let totalRange = self.maxDistance - self.minDistance + for _ in 0..<4 { + let min = self.minDistance + let max = self.minDistance + (totalRange * 0.12) + ranges.append((min, max)) + } + + for _ in 0..<4 { + let min = self.minDistance + (totalRange * 0.16) + let max = self.minDistance + (totalRange * 0.6) + ranges.append((min, max)) + } + + return ranges + } + + private func generateSinglePosition(viewSize: CGSize, minDist: CGFloat, maxDist: CGFloat, rightSide: Bool) -> CGPoint? { + let avatarCenter = avatarFrame.center + + for _ in 0..<50 { + let baseAngle: CGFloat + let angleSpread: CGFloat + + if rightSide { + baseAngle = 0 + angleSpread = .pi / 2 + } else { + baseAngle = .pi + angleSpread = .pi / 2 + } + + let angleOffset = (CGFloat(rng.next()) * 2.0 - 1.0) * angleSpread + let angle = baseAngle + angleOffset + + let distance = minDist + CGFloat(rng.next()) * (maxDist - minDist) + + let x = avatarCenter.x + cos(angle) * distance + let y = avatarCenter.y + sin(angle) * distance + + let position = CGPoint(x: x, y: y) + + let viewFrame = CGRect( + x: position.x - viewSize.width / 2, + y: position.y - viewSize.height / 2, + width: viewSize.width, + height: viewSize.height + ) + + if isFrameWithinBounds(viewFrame) && !isFrameInExclusionZone(viewFrame) { + return CGPoint(x: round(position.x), y: round(position.y)) + } + } + + return nil + } + + private func isFrameWithinBounds(_ frame: CGRect) -> Bool { + return frame.minX >= self.padding && + frame.minY >= self.padding && + frame.maxX <= self.containerSize.width - self.padding && + frame.maxY <= self.containerSize.height - self.padding + } + + private func isFrameInExclusionZone(_ frame: CGRect) -> Bool { + if frame.intersects(avatarFrame) { + return true + } + let padding: CGFloat = -8.0 + for excludeRect in self.excludeRects { + if frame.intersects(excludeRect.insetBy(dx: padding, dy: padding)) { + return true + } + } + return false + } +} + +private var shadowImage: UIImage? = { + return generateImage(CGSize(width: 44.0, height: 44.0), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + var locations: [CGFloat] = [0.0, 0.3, 1.0] + let colors: [CGColor] = [UIColor(rgb: 0xffffff, alpha: 0.65).cgColor, UIColor(rgb: 0xffffff, alpha: 0.65).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + context.drawRadialGradient(gradient, startCenter: CGPoint(x: size.width / 2.0, y: size.height / 2.0), startRadius: 0.0, endCenter: CGPoint(x: size.width / 2.0, y: size.height / 2.0), endRadius: size.width / 2.0, options: .drawsAfterEndLocation) + }) +}() + +private class GiftIconLayer: SimpleLayer { + private let context: AccountContext + private let gift: ProfileGiftsContext.State.StarGift + private let size: CGSize + + let shadowLayer = SimpleLayer() + let animationLayer: InlineStickerItemLayer + + override init(layer: Any) { + guard let layer = layer as? GiftIconLayer else { + fatalError() + } + + let context = layer.context + let gift = layer.gift + let size = layer.size + + var file: TelegramMediaFile? + var color: UIColor? + switch gift.gift { + case let .generic(gift): + file = gift.file + case let .unique(gift): + for attribute in gift.attributes { + if case let .model(_, fileValue, _) = attribute { + file = fileValue + } else if case let .backdrop(_, innerColor, _, _, _, _) = attribute { + color = UIColor(rgb: UInt32(bitPattern: innerColor)) + } + } + } + + let emoji = ChatTextInputTextCustomEmojiAttribute( + interactivelySelectedFromPackId: nil, + fileId: file?.fileId.id ?? 0, + file: file + ) + self.animationLayer = InlineStickerItemLayer( + context: .account(context), + userLocation: .other, + attemptSynchronousLoad: false, + emoji: emoji, + file: file, + cache: context.animationCache, + renderer: context.animationRenderer, + unique: true, + placeholderColor: UIColor.white.withAlphaComponent(0.2), + pointSize: CGSize(width: size.width * 2.0, height: size.height * 2.0), + loopCount: 1 + ) + + self.shadowLayer.contents = shadowImage?.cgImage + self.shadowLayer.layerTintColor = color?.cgColor + + self.context = context + self.gift = gift + self.size = size + + super.init() + + self.addSublayer(self.shadowLayer) + self.addSublayer(self.animationLayer) + } + + init( + context: AccountContext, + gift: ProfileGiftsContext.State.StarGift, + size: CGSize + ) { + self.context = context + self.gift = gift + self.size = size + + var file: TelegramMediaFile? + var color: UIColor? + switch gift.gift { + case let .generic(gift): + file = gift.file + case let .unique(gift): + for attribute in gift.attributes { + if case let .model(_, fileValue, _) = attribute { + file = fileValue + } else if case let .backdrop(_, innerColor, _, _, _, _) = attribute { + color = UIColor(rgb: UInt32(bitPattern: innerColor)) + } + } + } + + let emoji = ChatTextInputTextCustomEmojiAttribute( + interactivelySelectedFromPackId: nil, + fileId: file?.fileId.id ?? 0, + file: file + ) + self.animationLayer = InlineStickerItemLayer( + context: .account(context), + userLocation: .other, + attemptSynchronousLoad: false, + emoji: emoji, + file: file, + cache: context.animationCache, + renderer: context.animationRenderer, + unique: true, + placeholderColor: UIColor.white.withAlphaComponent(0.2), + pointSize: CGSize(width: size.width * 2.0, height: size.height * 2.0), + loopCount: 1 + ) + + self.shadowLayer.contents = shadowImage?.cgImage + self.shadowLayer.layerTintColor = color?.cgColor + + super.init() + + self.addSublayer(self.shadowLayer) + self.addSublayer(self.animationLayer) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + override func layoutSublayers() { + self.shadowLayer.frame = CGRect(origin: .zero, size: self.bounds.size).insetBy(dx: -4.0, dy: -4.0) + self.animationLayer.frame = CGRect(origin: .zero, size: self.bounds.size) + } + + func startHovering( + distance: CGFloat = 3.0, + duration: TimeInterval = 4.0, + timingFunction: CAMediaTimingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + ) { + let hoverAnimation = CABasicAnimation(keyPath: "transform.translation.y") + hoverAnimation.duration = duration + hoverAnimation.fromValue = -distance + hoverAnimation.toValue = distance + hoverAnimation.autoreverses = true + hoverAnimation.repeatCount = .infinity + hoverAnimation.timingFunction = timingFunction + hoverAnimation.beginTime = Double.random(in: 0.0 ..< 12.0) + hoverAnimation.isAdditive = true + self.add(hoverAnimation, forKey: "hover") + + let glowAnimation = CABasicAnimation(keyPath: "transform.scale") + glowAnimation.duration = duration + glowAnimation.fromValue = 1.0 + glowAnimation.toValue = 1.2 + glowAnimation.autoreverses = true + glowAnimation.repeatCount = .infinity + glowAnimation.timingFunction = timingFunction + glowAnimation.beginTime = Double.random(in: 0.0 ..< 12.0) + self.shadowLayer.add(glowAnimation, forKey: "glow") + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 782bb722a9..c35732d0e9 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -102,6 +102,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { let backgroundBannerView: UIView let backgroundCover = ComponentView() + let giftsCover = ComponentView() var didSetupBackgroundCover = false let buttonsContainerNode: SparseNode let buttonsBackgroundNode: NavigationBackgroundNode @@ -491,7 +492,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { private var currentStatusIcon: CredibilityIcon? private var currentPanelStatusData: PeerInfoStatusData? - func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, paneContainerY: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, threadData: MessageHistoryThreadData?, peerNotificationSettings: TelegramPeerNotificationSettings?, threadNotificationSettings: TelegramPeerNotificationSettings?, globalNotificationSettings: EngineGlobalNotificationSettings?, statusData: PeerInfoStatusData?, panelStatusData: (PeerInfoStatusData?, PeerInfoStatusData?, CGFloat?), isSecretChat: Bool, isContact: Bool, isSettings: Bool, state: PeerInfoState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition, additive: Bool, animateHeader: Bool) -> CGFloat { + func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, paneContainerY: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, threadData: MessageHistoryThreadData?, peerNotificationSettings: TelegramPeerNotificationSettings?, threadNotificationSettings: TelegramPeerNotificationSettings?, globalNotificationSettings: EngineGlobalNotificationSettings?, statusData: PeerInfoStatusData?, panelStatusData: (PeerInfoStatusData?, PeerInfoStatusData?, CGFloat?), isSecretChat: Bool, isContact: Bool, isSettings: Bool, state: PeerInfoState, profileGiftsContext: ProfileGiftsContext?, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition, additive: Bool, animateHeader: Bool) -> CGFloat { if self.appliedCustomNavigationContentNode !== self.customNavigationContentNode { if let previous = self.appliedCustomNavigationContentNode { transition.updateAlpha(node: previous, alpha: 0.0, completion: { [weak previous] _ in @@ -2336,6 +2337,35 @@ final class PeerInfoHeaderNode: ASDisplayNode { } } + if let profileGiftsContext, let peer { + let giftsCoverSize = self.giftsCover.update( + transition: ComponentTransition(transition), + component: AnyComponent(PeerInfoGiftsCoverComponent( + context: self.context, + peerId: peer.id, + giftsContext: profileGiftsContext, + avatarCenter: apparentAvatarFrame.center, + avatarScale: avatarScale, + defaultHeight: backgroundDefaultHeight, + avatarTransitionFraction: max(0.0, min(1.0, titleCollapseFraction + transitionFraction * 2.0)), + patternTransitionFraction: buttonsTransitionFraction * backgroundTransitionFraction, + hasButtons: !buttonKeys.isEmpty + )), + environment: {}, + containerSize: CGSize(width: width + bannerInset * 2.0, height: apparentBackgroundHeight + bannerInset) + ) + if let giftsCoverView = self.giftsCover.view as? PeerInfoGiftsCoverComponent.View { + if giftsCoverView.superview == nil { + self.backgroundBannerView.addSubview(giftsCoverView) + } + if additive { + transition.updateFrameAdditive(view: giftsCoverView, frame: CGRect(origin: CGPoint(x: -3.0, y: bannerFrame.height - giftsCoverSize.height - bannerInset), size: giftsCoverSize)) + } else { + transition.updateFrame(view: giftsCoverView, frame: CGRect(origin: CGPoint(x: 0.0, y: bannerFrame.height - giftsCoverSize.height - bannerInset), size: giftsCoverSize)) + } + } + } + if additive { transition.updateFrameAdditive(node: self.backgroundNode, frame: backgroundFrame) self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: transition) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 80838c7eba..39f2d35800 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -6300,13 +6300,17 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } if strongSelf.peerId.namespace == Namespaces.Peer.CloudUser && user.botInfo == nil && !user.flags.contains(.isSupport) { - items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_StartSecretChat, icon: { theme in - generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Lock"), color: theme.contextMenu.primaryColor) - }, action: { _, f in - f(.dismissWithoutContent) + if let cachedUserData = strongSelf.data?.cachedData as? CachedUserData, let _ = cachedUserData.sendPaidMessageStars { - self?.openStartSecretChat() - }))) + } else { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_StartSecretChat, icon: { theme in + generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Lock"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.dismissWithoutContent) + + self?.openStartSecretChat() + }))) + } } if user.botInfo == nil && data.isContact, let peer = strongSelf.data?.peer as? TelegramUser, let phone = peer.phone { @@ -11671,7 +11675,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let headerInset = sectionInset - let headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: self.headerNode.navigationTransition == nil ? transition : .immediate, additive: additive, animateHeader: transition.isAnimated && self.headerNode.navigationTransition == nil) + let headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, profileGiftsContext: self.data?.profileGiftsContext, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: self.headerNode.navigationTransition == nil ? transition : .immediate, additive: additive, animateHeader: transition.isAnimated && self.headerNode.navigationTransition == nil) let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: headerHeight)) if additive { transition.updateFrameAdditive(node: self.headerNode, frame: headerFrame) @@ -12056,7 +12060,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let headerInset = sectionInset - let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : offsetY, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: self.headerNode.navigationTransition == nil ? transition : .immediate, additive: additive, animateHeader: animateHeader && self.headerNode.navigationTransition == nil) + let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : offsetY, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, profileGiftsContext: self.data?.profileGiftsContext, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: self.headerNode.navigationTransition == nil ? transition : .immediate, additive: additive, animateHeader: animateHeader && self.headerNode.navigationTransition == nil) } let paneAreaExpansionDistance: CGFloat = 32.0 @@ -13691,7 +13695,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig } let headerInset = sectionInset - topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, contentOffset: 0.0, paneContainerY: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.savedMessagesPeer ?? self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, threadData: self.screenNode.data?.threadData, peerNotificationSettings: self.screenNode.data?.peerNotificationSettings, threadNotificationSettings: self.screenNode.data?.threadNotificationSettings, globalNotificationSettings: self.screenNode.data?.globalNotificationSettings, statusData: self.screenNode.data?.status, panelStatusData: (nil, nil, nil), isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: false, animateHeader: false) + topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, contentOffset: 0.0, paneContainerY: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.savedMessagesPeer ?? self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, threadData: self.screenNode.data?.threadData, peerNotificationSettings: self.screenNode.data?.peerNotificationSettings, threadNotificationSettings: self.screenNode.data?.threadNotificationSettings, globalNotificationSettings: self.screenNode.data?.globalNotificationSettings, statusData: self.screenNode.data?.status, panelStatusData: (nil, nil, nil), isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, profileGiftsContext: self.screenNode.data?.profileGiftsContext, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: false, animateHeader: false) } let titleScale = (fraction * previousTitleNode.view.bounds.height + (1.0 - fraction) * self.headerNode.titleNodeRawContainer.bounds.height) / previousTitleNode.view.bounds.height diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index c11468435f..866ea7e55a 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -455,7 +455,9 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else { switch transaction.peer { case let .peer(peer): - if transaction.flags.contains(.isPaidMessage) { + if let months = transaction.premiumGiftMonths { + titleText = strings.Stars_Transaction_TelegramPremium(months) + } else if transaction.flags.contains(.isPaidMessage) { isPaidMessage = true titleText = strings.Stars_Transaction_PaidMessage(transaction.paidMessageCount ?? 1) } else if !transaction.media.isEmpty { @@ -474,8 +476,13 @@ private final class StarsTransactionSheetContent: CombinedComponent { via = strings.Stars_Transaction_PremiumBotTopUp_Subtitle case .fragment: if parentPeer.id == component.context.account.peerId { - titleText = strings.Stars_Transaction_FragmentTopUp_Title - via = strings.Stars_Transaction_FragmentTopUp_Subtitle + if (transaction.count.value < 0 && !transaction.flags.contains(.isRefund)) || (transaction.count.value > 0 && transaction.flags.contains(.isRefund)) { + titleText = strings.Stars_Transaction_FragmentWithdrawal_Title + via = strings.Stars_Transaction_FragmentWithdrawal_Subtitle + } else { + titleText = strings.Stars_Transaction_FragmentTopUp_Title + via = strings.Stars_Transaction_FragmentTopUp_Subtitle + } } else { titleText = strings.Stars_Transaction_FragmentWithdrawal_Title via = strings.Stars_Transaction_FragmentWithdrawal_Subtitle diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index 935fb0a817..b5f9e00149 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -304,7 +304,10 @@ final class StarsTransactionsListPanelComponent: Component { var uniqueGift: StarGift.UniqueGift? switch item.peer { case let .peer(peer): - if item.flags.contains(.isPaidMessage) { + if let months = item.premiumGiftMonths { + itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) + itemSubtitle = environment.strings.Stars_Intro_Transaction_TelegramPremium(months) + } else 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 { @@ -353,8 +356,13 @@ final class StarsTransactionsListPanelComponent: Component { itemSubtitle = environment.strings.Stars_Intro_Transaction_Gift_Title itemPeer = .fragment } else { - itemTitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Title - itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Subtitle + if (item.count.value < 0 && !item.flags.contains(.isRefund)) || (item.count.value > 0 && item.flags.contains(.isRefund)) { + itemTitle = environment.strings.Stars_Intro_Transaction_FragmentWithdrawal_Title + itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentWithdrawal_Subtitle + } else { + itemTitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Title + itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Subtitle + } } } else { if item.count > StarsAmount.zero && !item.flags.contains(.isRefund) { From 3fb73a8b95553e815c526bfb09f974b3d9ca6203 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 28 Feb 2025 22:43:33 +0400 Subject: [PATCH 2/4] Various fixes --- .../Sources/PremiumIntroScreen.swift | 8 +- .../IncomingMessagePrivacyScreen.swift | 2 +- .../Sources/ChatUserInfoItem.swift | 21 +- .../Sources/PeerInfoGiftsCoverComponent.swift | 226 ++++++++++++------ .../PeerInfoScreen/Sources/PeerInfoData.swift | 4 +- .../Sources/PeerInfoHeaderNode.swift | 6 + 6 files changed, 176 insertions(+), 91 deletions(-) diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index a1585b91cf..7d0d43370a 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -885,13 +885,7 @@ struct PremiumIntroConfiguration { if perks.count < 4 { perks = PremiumIntroConfiguration.defaultValue.perks } - - #if DEBUG - if !perks.contains(.paidMessages) { - perks.append(.paidMessages) - } - #endif - + var businessPerks: [PremiumPerk] = [] if let values = data["business_promo_order"] as? [String] { for value in values { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift index b3be316415..0d6a43f888 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift @@ -356,7 +356,7 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP }, openPremiumInfo: { var replaceImpl: ((ViewController) -> Void)? - let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .paidMessages, forceDark: false, action: { + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .messagePrivacy, forceDark: false, action: { let controller = context.sharedContext.makePremiumIntroController(context: context, source: .paidMessages, forceDark: false, dismissed: nil) replaceImpl?(controller) }, dismissed: nil) diff --git a/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift index 9f95e83b2c..b20939179e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift @@ -219,11 +219,14 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe } public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - let location = gestureRecognizer.location(in: self.offsetContainer.view) - if let backgroundContent = self.backgroundContent, backgroundContent.frame.contains(location) { - return true + if gestureRecognizer.view === self.offsetContainer.view { + let location = gestureRecognizer.location(in: self.offsetContainer.view) + if let backgroundContent = self.backgroundContent, backgroundContent.frame.contains(location) { + return true + } + return false } - return false + return true } @objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) { @@ -366,7 +369,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe var estimatedValueOffset: CGFloat = 0.0 if groupsInCommonCount > 0 { groupsValueText = NSMutableAttributedString(string: item.presentationData.strings.Chat_NonContactUser_GroupsCount(groupsInCommonCount), font: Font.semibold(13.0), textColor: primaryTextColor) - estimatedValueOffset = avatarImageSize + CGFloat(min(2, max(0, item.groupsInCommonCount - 1))) * avatarSpacing + 4.0 + estimatedValueOffset = avatarImageSize + CGFloat(min(2, max(0, item.groupsInCommonCount - 1))) * avatarSpacing + 4.0 + 10.0 } else { groupsValueText = NSMutableAttributedString(string: "", font: Font.semibold(13.0), textColor: primaryTextColor) } @@ -380,7 +383,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe backgroundSize.height += groupsValueLayoutAndApply?.0.size.height ?? 0.0 maxTitleWidth = max(maxTitleWidth, groupsTitleLayoutAndApply?.0.size.width ?? 0) - maxValueWidth = max(maxValueWidth, groupsValueLayoutAndApply?.0.size.width ?? 0 + estimatedValueOffset) + maxValueWidth = max(maxValueWidth, (groupsValueLayoutAndApply?.0.size.width ?? 0) + estimatedValueOffset) } else { groupsTitleLayoutAndApply = nil groupsValueLayoutAndApply = nil @@ -474,15 +477,15 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe var attributeMidpoints: [CGFloat] = [] - func appendAttributeMidpoint(titleLayout: TextNodeLayout?, valueLayout: TextNodeLayout?) { + func appendAttributeMidpoint(titleLayout: TextNodeLayout?, valueLayout: TextNodeLayout?, valueOffset: CGFloat = 0.0) { if let valueLayout { - let midpoint = backgroundSize.width - horizontalContentInset - valueLayout.size.width - attributeSpacing / 2.0 + let midpoint = backgroundSize.width - horizontalContentInset - valueLayout.size.width - valueOffset - attributeSpacing / 2.0 attributeMidpoints.append(midpoint) } } appendAttributeMidpoint(titleLayout: phoneCountryTitleLayoutAndApply?.0, valueLayout: phoneCountryValueLayoutAndApply?.0) appendAttributeMidpoint(titleLayout: registrationDateTitleLayoutAndApply?.0, valueLayout: registrationDateValueLayoutAndApply?.0) - appendAttributeMidpoint(titleLayout: groupsTitleLayoutAndApply?.0, valueLayout: groupsValueLayoutAndApply?.0) + appendAttributeMidpoint(titleLayout: groupsTitleLayoutAndApply?.0, valueLayout: groupsValueLayoutAndApply?.0, valueOffset: estimatedValueOffset) let middleX = floorToScreenPixels(attributeMidpoints.min() ?? backgroundSize.width / 2.0) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift index b94da9ed13..605c2fcf9b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift @@ -63,6 +63,7 @@ public final class PeerInfoGiftsCoverComponent: Component { public let context: AccountContext public let peerId: EnginePeer.Id public let giftsContext: ProfileGiftsContext + public let hasBackground: Bool public let avatarCenter: CGPoint public let avatarScale: CGFloat public let defaultHeight: CGFloat @@ -74,6 +75,7 @@ public final class PeerInfoGiftsCoverComponent: Component { context: AccountContext, peerId: EnginePeer.Id, giftsContext: ProfileGiftsContext, + hasBackground: Bool, avatarCenter: CGPoint, avatarScale: CGFloat, defaultHeight: CGFloat, @@ -84,6 +86,7 @@ public final class PeerInfoGiftsCoverComponent: Component { self.context = context self.peerId = peerId self.giftsContext = giftsContext + self.hasBackground = hasBackground self.avatarCenter = avatarCenter self.avatarScale = avatarScale self.defaultHeight = defaultHeight @@ -99,6 +102,9 @@ public final class PeerInfoGiftsCoverComponent: Component { if lhs.peerId != rhs.peerId { return false } + if lhs.hasBackground != rhs.hasBackground { + return false + } if lhs.avatarCenter != rhs.avatarCenter { return false } @@ -202,11 +208,11 @@ public final class PeerInfoGiftsCoverComponent: Component { avatarFrame: CGSize(width: 100, height: 100).centered(around: component.avatarCenter), minDistance: 75.0, maxDistance: availableSize.width / 2.0, - padding: 16.0, + padding: 12.0, seed: self.seed, excludeRects: excludeRects ) - self.iconPositions = positionGenerator.generatePositions(count: 6, viewSize: iconSize) + self.iconPositions = positionGenerator.generatePositions(count: 9, viewSize: iconSize) } if self.giftsDisposable == nil { @@ -256,7 +262,7 @@ public final class PeerInfoGiftsCoverComponent: Component { var validIds = Set() var index = 0 - for gift in self.gifts.prefix(6) { + for gift in self.gifts.prefix(9) { let id: AnyHashable if case let .unique(uniqueGift) = gift.gift { id = uniqueGift.slug @@ -270,7 +276,7 @@ public final class PeerInfoGiftsCoverComponent: Component { if let current = self.iconLayers[id] { iconLayer = current } else { - iconLayer = GiftIconLayer(context: component.context, gift: gift, size: iconSize) + iconLayer = GiftIconLayer(context: component.context, gift: gift, size: iconSize, glowing: component.hasBackground) iconLayer.startHovering() self.iconLayers[id] = iconLayer self.layer.addSublayer(iconLayer) @@ -279,8 +285,8 @@ public final class PeerInfoGiftsCoverComponent: Component { iconLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2) } - let zeroPosition = component.avatarCenter - let finalPosition = iconPosition.center + let centerPosition = component.avatarCenter + let finalPosition = iconPosition.center.offsetBy(dx: component.avatarCenter.x, dy: component.avatarCenter.y) let itemScaleFraction = patternScaleValueAt(fraction: component.avatarTransitionFraction, t: 0.0, reverse: false) func interpolateRect(from: CGPoint, to: CGPoint, t: CGFloat) -> CGPoint { @@ -295,7 +301,7 @@ public final class PeerInfoGiftsCoverComponent: Component { ) } - let effectivePosition = interpolateRect(from: finalPosition, to: zeroPosition, t: itemScaleFraction) + let effectivePosition = interpolateRect(from: finalPosition, to: centerPosition, t: itemScaleFraction) transition.setBounds(layer: iconLayer, bounds: CGRect(origin: .zero, size: iconSize)) transition.setPosition(layer: iconLayer, position: effectivePosition) @@ -350,10 +356,10 @@ private class PositionGenerator { init( containerSize: CGSize, avatarFrame: CGRect, - minDistance: CGFloat = 20, - maxDistance: CGFloat = 100, - padding: CGFloat = 10, - seed: UInt = UInt.random(in: 0 ..< 10), + minDistance: CGFloat, + maxDistance: CGFloat, + padding: CGFloat, + seed: UInt, excludeRects: [CGRect] = [] ) { self.containerSize = containerSize @@ -376,13 +382,13 @@ private class PositionGenerator { let maxDist = distanceRanges[i].1 let isEven = i % 2 == 0 - // Try to generate a valid position with multiple attempts var attempts = 0 let maxAttempts = 20 - var foundPosition = false var currentMaxDist = maxDist - while !foundPosition && attempts < maxAttempts { + var result: CGPoint? + + while result == nil && attempts < maxAttempts { attempts += 1 if let position = generateSinglePosition( @@ -391,54 +397,55 @@ private class PositionGenerator { maxDist: currentMaxDist, rightSide: !isEven ) { - // Skip distance check if this is the first position let isFarEnough = positions.isEmpty || positions.allSatisfy { existingPosition in let distance = hypot(position.x - existingPosition.center.x, position.y - existingPosition.center.y) - return distance > (viewSize.width + padding) + let minRequiredDistance = max(viewSize.width, viewSize.height) / 2 + max(viewSize.width, viewSize.height) / 2 + padding + return distance > minRequiredDistance } - - let distance = hypot(position.x - self.avatarFrame.center.x, position.y - self.avatarFrame.center.y) - var scale = max(1.0, min(0.55, 1.0 - (distance - 50.0) / 100.0)) - scale = scale * scale - if isFarEnough { - positions.append(Position(center: position, scale: scale)) - foundPosition = true + result = position break } } - if attempts % 5 == 0 && !foundPosition { + if attempts % 5 == 0 && result == nil { currentMaxDist *= 1.2 } } - if !foundPosition { - if let lastChancePosition = generateSinglePosition( + if result == nil { + if let lastChancePosition = self.generateSinglePosition( viewSize: viewSize, minDist: minDist, - maxDist: maxDist * 2.0, // Try with a much larger distance + maxDist: maxDist * 2.0, rightSide: !isEven ) { - let distance = hypot(lastChancePosition.x - self.avatarFrame.center.x, - lastChancePosition.y - self.avatarFrame.center.y) - var scale = max(1.0, min(0.55, 1.0 - (distance - 50.0) / 100.0)) - scale = scale * scale - - positions.append(Position(center: lastChancePosition, scale: scale)) + result = lastChancePosition } else { - // If all else fails, create a position with default values to ensure we don't return fewer positions than requested let defaultX = self.avatarFrame.center.x + (isEven ? -1 : 1) * (minDist + CGFloat(i * 20)) let defaultY = self.avatarFrame.center.y + CGFloat(i * 15) let defaultPosition = CGPoint(x: defaultX, y: defaultY) - // Use a smaller scale for these fallback positions - positions.append(Position(center: defaultPosition, scale: 0.5)) + result = defaultPosition } } + + if let result { + let distance = hypot(result.x - self.avatarFrame.center.x, result.y - self.avatarFrame.center.y) + let baseScale = min(1.0, max(0.77, 1.0 - (distance - 75.0) / 75.0)) + + let randomFactor = 0.05 + (1.0 - baseScale) * 0.1 + let randomValue = -randomFactor + CGFloat(self.rng.next()) * 2.0 * randomFactor + + let finalScale = min(1.1, max(baseScale * 0.7, baseScale + randomValue)) + + positions.append(Position(center: result, scale: finalScale)) + } } - return positions + return positions.map { + Position(center: $0.center.offsetBy(dx: -self.avatarFrame.center.x, dy: -self.avatarFrame.center.y), scale: $0.scale) + } } private func calculateDistanceRanges(count: Int) -> [(CGFloat, CGFloat)] { @@ -452,7 +459,7 @@ private class PositionGenerator { } for _ in 0..<4 { - let min = self.minDistance + (totalRange * 0.16) + let min = self.minDistance + (totalRange * 0.19) let max = self.minDistance + (totalRange * 0.6) ranges.append((min, max)) } @@ -533,25 +540,83 @@ private var shadowImage: UIImage? = { }) }() +private final class StarsEffectLayer: SimpleLayer { + private let emitterLayer = CAEmitterLayer() + + override init() { + super.init() + + self.addSublayer(self.emitterLayer) + } + + override init(layer: Any) { + super.init(layer: layer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setup(color: UIColor, size: CGSize) { + let emitter = CAEmitterCell() + emitter.name = "emitter" + emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage + emitter.birthRate = 8.0 + emitter.lifetime = 2.0 + emitter.velocity = 0.1 + emitter.scale = (size.width / 40.0) * 0.12 + emitter.scaleRange = 0.02 + emitter.alphaRange = 0.1 + emitter.emissionRange = .pi * 2.0 + + let staticColors: [Any] = [ + color.withAlphaComponent(0.0).cgColor, + color.withAlphaComponent(0.58).cgColor, + color.withAlphaComponent(0.58).cgColor, + color.withAlphaComponent(0.0).cgColor + ] + let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife") + staticColorBehavior.setValue(staticColors, forKey: "colors") + emitter.setValue([staticColorBehavior], forKey: "emitterBehaviors") + self.emitterLayer.emitterCells = [emitter] + } + + func update(color: UIColor, size: CGSize) { + if self.emitterLayer.emitterCells == nil { + self.setup(color: color, size: size) + } + self.emitterLayer.seed = UInt32.random(in: .min ..< .max) + self.emitterLayer.emitterShape = .circle + self.emitterLayer.emitterSize = size + self.emitterLayer.emitterMode = .surface + self.emitterLayer.frame = CGRect(origin: .zero, size: size) + self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + } +} + + private class GiftIconLayer: SimpleLayer { private let context: AccountContext private let gift: ProfileGiftsContext.State.StarGift private let size: CGSize + private let glowing: Bool let shadowLayer = SimpleLayer() + let starsLayer = StarsEffectLayer() let animationLayer: InlineStickerItemLayer override init(layer: Any) { guard let layer = layer as? GiftIconLayer else { fatalError() } - + let context = layer.context let gift = layer.gift let size = layer.size - + let glowing = layer.glowing + var file: TelegramMediaFile? - var color: UIColor? + var color: UIColor = .white switch gift.gift { case let .generic(gift): file = gift.file @@ -585,29 +650,40 @@ private class GiftIconLayer: SimpleLayer { ) self.shadowLayer.contents = shadowImage?.cgImage - self.shadowLayer.layerTintColor = color?.cgColor + self.shadowLayer.layerTintColor = color.cgColor self.context = context self.gift = gift self.size = size + self.glowing = glowing super.init() - self.addSublayer(self.shadowLayer) + let side = floor(size.width * 1.25) + let starsFrame = CGSize(width: side, height: side).centered(in: CGRect(origin: .zero, size: size)) + self.starsLayer.frame = starsFrame + self.starsLayer.update(color: glowing ? .white : color, size: starsFrame.size) + + if glowing { + self.addSublayer(self.shadowLayer) + } + self.addSublayer(self.starsLayer) self.addSublayer(self.animationLayer) } init( context: AccountContext, gift: ProfileGiftsContext.State.StarGift, - size: CGSize + size: CGSize, + glowing: Bool ) { self.context = context self.gift = gift self.size = size + self.glowing = glowing var file: TelegramMediaFile? - var color: UIColor? + var color: UIColor = .white switch gift.gift { case let .generic(gift): file = gift.file @@ -641,11 +717,19 @@ private class GiftIconLayer: SimpleLayer { ) self.shadowLayer.contents = shadowImage?.cgImage - self.shadowLayer.layerTintColor = color?.cgColor + self.shadowLayer.layerTintColor = color.cgColor super.init() - self.addSublayer(self.shadowLayer) + let side = floor(size.width * 1.25) + let starsFrame = CGSize(width: side, height: side).centered(in: CGRect(origin: .zero, size: size)) + self.starsLayer.frame = starsFrame + self.starsLayer.update(color: glowing ? .white : color, size: starsFrame.size) + + if glowing { + self.addSublayer(self.shadowLayer) + } + self.addSublayer(self.starsLayer) self.addSublayer(self.animationLayer) } @@ -658,30 +742,26 @@ private class GiftIconLayer: SimpleLayer { self.animationLayer.frame = CGRect(origin: .zero, size: self.bounds.size) } - func startHovering( - distance: CGFloat = 3.0, - duration: TimeInterval = 4.0, - timingFunction: CAMediaTimingFunction = CAMediaTimingFunction(name: .easeInEaseOut) - ) { - let hoverAnimation = CABasicAnimation(keyPath: "transform.translation.y") - hoverAnimation.duration = duration - hoverAnimation.fromValue = -distance - hoverAnimation.toValue = distance - hoverAnimation.autoreverses = true - hoverAnimation.repeatCount = .infinity - hoverAnimation.timingFunction = timingFunction - hoverAnimation.beginTime = Double.random(in: 0.0 ..< 12.0) - hoverAnimation.isAdditive = true - self.add(hoverAnimation, forKey: "hover") - - let glowAnimation = CABasicAnimation(keyPath: "transform.scale") - glowAnimation.duration = duration - glowAnimation.fromValue = 1.0 - glowAnimation.toValue = 1.2 - glowAnimation.autoreverses = true - glowAnimation.repeatCount = .infinity - glowAnimation.timingFunction = timingFunction - glowAnimation.beginTime = Double.random(in: 0.0 ..< 12.0) - self.shadowLayer.add(glowAnimation, forKey: "glow") - } + func startHovering(distance: CGFloat = 3.0, duration: TimeInterval = 4.0, timingFunction: CAMediaTimingFunction = CAMediaTimingFunction(name: .easeInEaseOut)) { + let hoverAnimation = CABasicAnimation(keyPath: "transform.translation.y") + hoverAnimation.duration = duration + hoverAnimation.fromValue = -distance + hoverAnimation.toValue = distance + hoverAnimation.autoreverses = true + hoverAnimation.repeatCount = .infinity + hoverAnimation.timingFunction = timingFunction + hoverAnimation.beginTime = Double.random(in: 0.0 ..< 12.0) + hoverAnimation.isAdditive = true + self.add(hoverAnimation, forKey: "hover") + + let glowAnimation = CABasicAnimation(keyPath: "transform.scale") + glowAnimation.duration = duration + glowAnimation.fromValue = 1.0 + glowAnimation.toValue = 1.2 + glowAnimation.autoreverses = true + glowAnimation.repeatCount = .infinity + glowAnimation.timingFunction = timingFunction + glowAnimation.beginTime = Double.random(in: 0.0 ..< 12.0) + self.shadowLayer.add(glowAnimation, forKey: "glow") + } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 35ac01baea..c13ba1dd16 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -836,6 +836,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, starsState = .single(nil) } + let profileGiftsContext = ProfileGiftsContext(account: context.account, peerId: peerId) + return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), accountsAndPeers, @@ -945,7 +947,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, starsRevenueStatsContext: nil, revenueStatsState: nil, revenueStatsContext: nil, - profileGiftsContext: nil, + profileGiftsContext: profileGiftsContext, premiumGiftOptions: [], webAppPermissions: nil ) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index c35732d0e9..a33fa58dab 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -2277,6 +2277,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { let backgroundCoverSubject: PeerInfoCoverComponent.Subject? var backgroundCoverAnimateIn = false var backgroundDefaultHeight: CGFloat = 254.0 + var hasBackground = false if let status = peer?.emojiStatus, case .starGift = status.content { backgroundCoverSubject = .status(status) if !self.didSetupBackgroundCover { @@ -2288,8 +2289,12 @@ final class PeerInfoHeaderNode: ASDisplayNode { if !buttonKeys.isEmpty { backgroundDefaultHeight = 327.0 } + hasBackground = true } else if let peer { backgroundCoverSubject = .peer(EnginePeer(peer)) + if peer.profileColor != nil { + hasBackground = true + } } else { backgroundCoverSubject = nil } @@ -2344,6 +2349,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { context: self.context, peerId: peer.id, giftsContext: profileGiftsContext, + hasBackground: hasBackground, avatarCenter: apparentAvatarFrame.center, avatarScale: avatarScale, defaultHeight: backgroundDefaultHeight, From 86e7238bfaa10b817293a43546e4de9f62f75dcb Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 28 Feb 2025 23:07:50 +0400 Subject: [PATCH 3/4] Various fixes --- .../Sources/PeerInfoGiftsCoverComponent.swift | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift index 605c2fcf9b..ddb00e1961 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift @@ -302,10 +302,10 @@ public final class PeerInfoGiftsCoverComponent: Component { } let effectivePosition = interpolateRect(from: finalPosition, to: centerPosition, t: itemScaleFraction) - + transition.setBounds(layer: iconLayer, bounds: CGRect(origin: .zero, size: iconSize)) transition.setPosition(layer: iconLayer, position: effectivePosition) - transition.setScale(layer: iconLayer, scale: iconPosition.scale) + transition.setScale(layer: iconLayer, scale: iconPosition.scale * (1.0 - itemScaleFraction)) transition.setAlpha(layer: iconLayer, alpha: 1.0 - itemScaleFraction) index += 1 @@ -437,8 +437,7 @@ private class PositionGenerator { let randomFactor = 0.05 + (1.0 - baseScale) * 0.1 let randomValue = -randomFactor + CGFloat(self.rng.next()) * 2.0 * randomFactor - let finalScale = min(1.1, max(baseScale * 0.7, baseScale + randomValue)) - + let finalScale = min(1.2, max(baseScale * 0.65, baseScale + randomValue)) positions.append(Position(center: result, scale: finalScale)) } } @@ -460,7 +459,13 @@ private class PositionGenerator { for _ in 0..<4 { let min = self.minDistance + (totalRange * 0.19) - let max = self.minDistance + (totalRange * 0.6) + let max = self.minDistance + (totalRange * 0.55) + ranges.append((min, max)) + } + + for _ in 0..<4 { + let min = self.minDistance + (totalRange * 0.6) + let max = self.minDistance + (totalRange * 0.9) ranges.append((min, max)) } From 15fd6d7b37d0a2bbd522cf22ec0cdfdb595144e3 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 1 Mar 2025 00:49:18 +0400 Subject: [PATCH 4/4] Various fixes --- .../Sources/LegacyPaintStickersContext.swift | 11 +++ .../Sources/MediaPickerScreen.swift | 7 +- .../Sources/MessageInputPanelComponent.swift | 2 +- .../Sources/PeerInfoGiftsCoverComponent.swift | 67 ++++++++++++++----- .../Chat/ChatControllerPaidMessage.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 12 ++-- .../ChatTextInputActionButtonsNode.swift | 2 +- .../Sources/ChatTextInputPanelNode.swift | 4 +- 8 files changed, 80 insertions(+), 27 deletions(-) diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index 9f30f07350..af525ff44f 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -629,6 +629,7 @@ private class SendStarsButtonView: HighlightTrackingButton, TGPhotoSendStarsButt override init(frame: CGRect) { self.backgroundView = UIView() + self.backgroundView.isUserInteractionEnabled = false self.textNode = ImmediateAnimatedCountLabelNode() self.textNode.isUserInteractionEnabled = false @@ -654,12 +655,22 @@ private class SendStarsButtonView: HighlightTrackingButton, TGPhotoSendStarsButt self.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } + + self.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) } required init?(coder: NSCoder) { preconditionFailure() } + deinit { + print() + } + + @objc private func buttonPressed() { + self.pressed?() + } + func updateFrame(_ frame: CGRect) { let transition: ContainedViewLayoutTransition if self.frame.width.isZero { diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 86aca15324..4a6be111a9 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -1322,7 +1322,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att } } - let proceed: (Bool) -> Void = { convertToJpeg in + let proceed: (Bool) -> Void = { [weak self] convertToJpeg in let signals: [Any]! switch controller.subject { case .assets: @@ -1340,6 +1340,11 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att completion() self?.controller?.dismiss(animated: animated) }) + + Queue.mainQueue().after(1.5) { + controller.isDismissing = false + controller.completed = false + } } if asFile && hasHeic { diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index ad21bcfc7d..290e126b5e 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -1452,7 +1452,7 @@ public final class MessageInputPanelComponent: Component { inputActionButtonMode = .send } else { if self.textFieldExternalState.hasText { - if let sendPaidMessageStars = component.sendPaidMessageStars { + if let sendPaidMessageStars = component.sendPaidMessageStars, "".isEmpty { inputActionButtonMode = .stars(sendPaidMessageStars.value) } else { inputActionButtonMode = .send diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift index ddb00e1961..267befaacd 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift @@ -138,11 +138,11 @@ public final class PeerInfoGiftsCoverComponent: Component { private var giftsDisposable: Disposable? private var gifts: [ProfileGiftsContext.State.StarGift] = [] + private var appliedGiftIds: [Int64] = [] private var iconLayers: [AnyHashable: GiftIconLayer] = [:] private var iconPositions: [PositionGenerator.Position] = [] - private let seed = UInt(Date().timeIntervalSince1970) override public init(frame: CGRect) { self.avatarBackgroundGradientLayer = SimpleGradientLayer() @@ -185,6 +185,7 @@ public final class PeerInfoGiftsCoverComponent: Component { self.isUpdating = false } + let previousComponent = self.component self.component = component self.state = state @@ -193,7 +194,15 @@ public final class PeerInfoGiftsCoverComponent: Component { let iconSize = CGSize(width: 32.0, height: 32.0) - if previousCurrentSize?.width != availableSize.width { + let giftIds = self.gifts.map { gift in + if case let .unique(gift) = gift.gift { + return gift.id + } else { + return 0 + } + } + + if previousCurrentSize?.width != availableSize.width || (previousComponent != nil && previousComponent?.hasBackground != component.hasBackground) || self.appliedGiftIds != giftIds { var excludeRects: [CGRect] = [] excludeRects.append(CGRect(origin: .zero, size: CGSize(width: 50.0, height: 90.0))) excludeRects.append(CGRect(origin: CGPoint(x: availableSize.width - 105.0, y: 0.0), size: CGSize(width: 105.0, height: 90.0))) @@ -209,11 +218,12 @@ public final class PeerInfoGiftsCoverComponent: Component { minDistance: 75.0, maxDistance: availableSize.width / 2.0, padding: 12.0, - seed: self.seed, + seed: UInt(Date().timeIntervalSince1970), excludeRects: excludeRects ) self.iconPositions = positionGenerator.generatePositions(count: 9, viewSize: iconSize) } + self.appliedGiftIds = giftIds if self.giftsDisposable == nil { self.giftsDisposable = combineLatest( @@ -243,7 +253,7 @@ public final class PeerInfoGiftsCoverComponent: Component { self.gifts = pinnedGifts if !self.isUpdating { - self.state?.updated(transition: .immediate) + self.state?.updated(transition: .spring(duration: 0.4)) } }) } @@ -271,11 +281,13 @@ public final class PeerInfoGiftsCoverComponent: Component { } validIds.insert(id) + var iconTransition = transition let iconPosition = self.iconPositions[index] let iconLayer: GiftIconLayer if let current = self.iconLayers[id] { iconLayer = current } else { + iconTransition = .immediate iconLayer = GiftIconLayer(context: component.context, gift: gift, size: iconSize, glowing: component.hasBackground) iconLayer.startHovering() self.iconLayers[id] = iconLayer @@ -284,6 +296,7 @@ public final class PeerInfoGiftsCoverComponent: Component { iconLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) iconLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2) } + iconLayer.glowing = component.hasBackground let centerPosition = component.avatarCenter let finalPosition = iconPosition.center.offsetBy(dx: component.avatarCenter.x, dy: component.avatarCenter.y) @@ -303,10 +316,10 @@ public final class PeerInfoGiftsCoverComponent: Component { let effectivePosition = interpolateRect(from: finalPosition, to: centerPosition, t: itemScaleFraction) - transition.setBounds(layer: iconLayer, bounds: CGRect(origin: .zero, size: iconSize)) - transition.setPosition(layer: iconLayer, position: effectivePosition) - transition.setScale(layer: iconLayer, scale: iconPosition.scale * (1.0 - itemScaleFraction)) - transition.setAlpha(layer: iconLayer, alpha: 1.0 - itemScaleFraction) + iconTransition.setBounds(layer: iconLayer, bounds: CGRect(origin: .zero, size: iconSize)) + iconTransition.setPosition(layer: iconLayer, position: effectivePosition) + iconTransition.setScale(layer: iconLayer, scale: iconPosition.scale * (1.0 - itemScaleFraction)) + iconTransition.setAlpha(layer: iconLayer, alpha: 1.0 - itemScaleFraction) index += 1 } @@ -434,7 +447,7 @@ private class PositionGenerator { let distance = hypot(result.x - self.avatarFrame.center.x, result.y - self.avatarFrame.center.y) let baseScale = min(1.0, max(0.77, 1.0 - (distance - 75.0) / 75.0)) - let randomFactor = 0.05 + (1.0 - baseScale) * 0.1 + let randomFactor = 0.14 + (1.0 - baseScale) * 0.2 let randomValue = -randomFactor + CGFloat(self.rng.next()) * 2.0 * randomFactor let finalScale = min(1.2, max(baseScale * 0.65, baseScale + randomValue)) @@ -563,6 +576,8 @@ private final class StarsEffectLayer: SimpleLayer { } func setup(color: UIColor, size: CGSize) { + self.color = color + let emitter = CAEmitterCell() emitter.name = "emitter" emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage @@ -586,8 +601,10 @@ private final class StarsEffectLayer: SimpleLayer { self.emitterLayer.emitterCells = [emitter] } + private var color: UIColor? + func update(color: UIColor, size: CGSize) { - if self.emitterLayer.emitterCells == nil { + if self.color != color { self.setup(color: color, size: size) } self.emitterLayer.seed = UInt32.random(in: .min ..< .max) @@ -604,7 +621,25 @@ private class GiftIconLayer: SimpleLayer { private let context: AccountContext private let gift: ProfileGiftsContext.State.StarGift private let size: CGSize - private let glowing: Bool + var glowing: Bool { + didSet { + self.shadowLayer.opacity = self.glowing ? 1.0 : 0.0 + + let color: UIColor + if self.glowing { + color = .white + } else if let layerTintColor = self.shadowLayer.layerTintColor { + color = UIColor(cgColor: layerTintColor) + } else { + color = .white + } + + let side = floor(self.size.width * 1.25) + let starsFrame = CGSize(width: side, height: side).centered(in: CGRect(origin: .zero, size: self.size)) + self.starsLayer.frame = starsFrame + self.starsLayer.update(color: color, size: starsFrame.size) + } + } let shadowLayer = SimpleLayer() let starsLayer = StarsEffectLayer() @@ -656,6 +691,7 @@ private class GiftIconLayer: SimpleLayer { self.shadowLayer.contents = shadowImage?.cgImage self.shadowLayer.layerTintColor = color.cgColor + self.shadowLayer.opacity = glowing ? 1.0 : 0.0 self.context = context self.gift = gift @@ -669,9 +705,7 @@ private class GiftIconLayer: SimpleLayer { self.starsLayer.frame = starsFrame self.starsLayer.update(color: glowing ? .white : color, size: starsFrame.size) - if glowing { - self.addSublayer(self.shadowLayer) - } + self.addSublayer(self.shadowLayer) self.addSublayer(self.starsLayer) self.addSublayer(self.animationLayer) } @@ -723,6 +757,7 @@ private class GiftIconLayer: SimpleLayer { self.shadowLayer.contents = shadowImage?.cgImage self.shadowLayer.layerTintColor = color.cgColor + self.shadowLayer.opacity = glowing ? 1.0 : 0.0 super.init() @@ -731,9 +766,7 @@ private class GiftIconLayer: SimpleLayer { self.starsLayer.frame = starsFrame self.starsLayer.update(color: glowing ? .white : color, size: starsFrame.size) - if glowing { - self.addSublayer(self.shadowLayer) - } + self.addSublayer(self.shadowLayer) self.addSublayer(self.starsLayer) self.addSublayer(self.animationLayer) } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift index 0833193f98..5aa4ed7a50 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift @@ -20,7 +20,7 @@ extension ChatControllerImpl { completion(false) return } - if let sendPaidMessageStars = self.presentationInterfaceState.sendPaidMessageStars { + if let sendPaidMessageStars = self.presentationInterfaceState.sendPaidMessageStars, self.presentationInterfaceState.interfaceState.editMessage == nil { let totalAmount = sendPaidMessageStars.value * Int64(count) let _ = (ApplicationSpecificNotice.dismissedPaidMessageWarningNamespace(accountManager: self.context.sharedContext.accountManager, peerId: peer.id) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index ba2d94f03b..a86ba9fe77 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2067,11 +2067,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } }, sendCurrentMessage: { [weak self] silentPosting, messageEffect in - if let strongSelf = self { - if let _ = strongSelf.presentationInterfaceState.interfaceState.mediaDraftState { - strongSelf.sendMediaRecording(silentPosting: silentPosting, messageEffect: messageEffect) + if let self { + if let _ = self.presentationInterfaceState.interfaceState.mediaDraftState { + self.sendMediaRecording(silentPosting: silentPosting, messageEffect: messageEffect) } else { - strongSelf.chatDisplayNode.sendCurrentMessage(silentPosting: silentPosting, messageEffect: messageEffect) + self.presentPaidMessageAlertIfNeeded(count: 1, completion: { [weak self] postpone in + if let self { + self.chatDisplayNode.sendCurrentMessage(silentPosting: silentPosting, postpone: postpone, messageEffect: messageEffect) + } + }) } } }, sendMessage: { [weak self] text in diff --git a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift b/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift index 5d83f3b04b..a831f1cab1 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift @@ -272,7 +272,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction self.validLayout = size var innerSize = size - if let sendPaidMessageStars = interfaceState.sendPaidMessageStars { + if let sendPaidMessageStars = interfaceState.sendPaidMessageStars, interfaceState.interfaceState.editMessage == nil { self.sendButton.imageNode.alpha = 0.0 self.textNode.isHidden = false diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 129a29154f..18fcdf4e72 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -1915,7 +1915,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch placeholder = interfaceState.strings.Conversation_InputTextAnonymousPlaceholder } else if case let .replyThread(replyThreadMessage) = interfaceState.chatLocation, !replyThreadMessage.isForumPost, replyThreadMessage.peerId != self.context?.account.peerId { if replyThreadMessage.isChannelPost { - if let sendPaidMessageStars = interfaceState.sendPaidMessageStars { + if let sendPaidMessageStars = interfaceState.sendPaidMessageStars, interfaceState.interfaceState.editMessage == nil { placeholder = interfaceState.strings.Chat_InputTextPaidCommentPlaceholder(" # \(presentationStringsFormattedNumber(Int32(sendPaidMessageStars.value), interfaceState.dateTimeFormat.groupingSeparator))").string placeholderHasStar = true } else { @@ -1931,7 +1931,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch placeholder = interfaceState.strings.Chat_InputPlaceholderMessageInTopic(forumTopicData.title).string } } else { - if let sendPaidMessageStars = interfaceState.sendPaidMessageStars { + if let sendPaidMessageStars = interfaceState.sendPaidMessageStars, interfaceState.interfaceState.editMessage == nil { placeholder = interfaceState.strings.Chat_InputTextPaidMessagePlaceholder(" # \(presentationStringsFormattedNumber(Int32(sendPaidMessageStars.value), interfaceState.dateTimeFormat.groupingSeparator))").string placeholderHasStar = true } else {