Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2024-08-09 17:43:55 +02:00
commit fa492bd41a
26 changed files with 629 additions and 174 deletions

View File

@ -1524,7 +1524,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
}
switch result {
case let .done(receiptMessageId):
case let .done(receiptMessageId, _):
proceedWithCompletion(true, receiptMessageId)
case let .externalVerificationRequired(url):
strongSelf.updateActionButton()

View File

@ -345,7 +345,7 @@ public struct ComponentTransition {
}
}
public func setPosition(view: UIView, position: CGPoint, completion: ((Bool) -> Void)? = nil) {
public func setPosition(view: UIView, position: CGPoint, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
if view.center == position {
completion?(true)
return
@ -364,7 +364,7 @@ public struct ComponentTransition {
}
view.center = position
self.animatePosition(view: view, from: previousPosition, to: view.center, completion: completion)
self.animatePosition(view: view, from: previousPosition, to: view.center, delay: delay, completion: completion)
}
}
@ -803,8 +803,8 @@ public struct ComponentTransition {
}
}
public func animatePosition(view: UIView, from fromValue: CGPoint, to toValue: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
self.animatePosition(layer: view.layer, from: fromValue, to: toValue, additive: additive, completion: completion)
public func animatePosition(view: UIView, from fromValue: CGPoint, to toValue: CGPoint, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
self.animatePosition(layer: view.layer, from: fromValue, to: toValue, delay: delay, additive: additive, completion: completion)
}
public func animateBounds(view: UIView, from fromValue: CGRect, to toValue: CGRect, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
@ -819,7 +819,7 @@ public struct ComponentTransition {
self.animateBoundsSize(layer: view.layer, from: fromValue, to: toValue, additive: additive, completion: completion)
}
public func animatePosition(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
public func animatePosition(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
switch self.animation {
case .none:
completion?(true)
@ -829,7 +829,7 @@ public struct ComponentTransition {
to: NSValue(cgPoint: toValue),
keyPath: "position",
duration: duration,
delay: 0.0,
delay: delay,
curve: curve,
removeOnCompletion: true,
additive: additive,

View File

@ -25,6 +25,7 @@ swift_library(
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TextFormat:TextFormat",
"//submodules/AppBundle",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
],
visibility = [
"//visibility:public",

View File

@ -15,6 +15,7 @@ import MultiAnimationRenderer
import EmojiTextAttachmentView
import TextFormat
import AppBundle
import AnimatedTextComponent
private let tagImage: UIImage? = {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ReactionTagBackground"), color: .white)?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 15)
@ -832,6 +833,12 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
private var ignoreButtonTap: Bool = false
private var tapAnimationLink: SharedDisplayLinkDriver.Link?
private var tapAnimationValue: CGFloat = 0.0
private var previousTapAnimationTimestamp: Double = 0.0
private var previousTapTimestamp: Double = 0.0
private var tapCounterView: StarsReactionCounterView?
public var activateAfterCompletion: Bool = false {
didSet {
if self.activateAfterCompletion {
@ -931,13 +938,101 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
return
}
layout.spec.component.action(self, layout.spec.component.reaction.value, self.containerView)
if case .stars = layout.spec.component.reaction.value {
self.addStarsTap()
}
}
private func addStarsTap() {
let timestamp = CACurrentMediaTime()
self.previousTapTimestamp = timestamp
let deltaTime = timestamp - self.previousTapAnimationTimestamp
if deltaTime < 0.4 || self.tapCounterView != nil {
self.previousTapAnimationTimestamp = timestamp
if let superview = self.superview {
for subview in superview.subviews {
if subview !== self {
subview.layer.zPosition = 0.0
}
}
}
self.layer.zPosition = 1.0
if let tapCounterView = self.tapCounterView {
tapCounterView.add()
} else {
let tapCounterView = StarsReactionCounterView(count: 2)
self.tapCounterView = tapCounterView
self.addSubview(tapCounterView)
tapCounterView.animateIn()
if let layout = self.layout {
tapCounterView.frame = CGRect(origin: CGPoint(x: layout.size.width * 0.5, y: -70.0), size: CGSize())
}
}
}
self.tapAnimationValue = min(1.0, self.tapAnimationValue)
if self.tapAnimationLink == nil {
self.previousTapAnimationTimestamp = timestamp
self.updateTapAnimation()
self.tapAnimationLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in
guard let self else {
return
}
self.updateTapAnimation()
})
}
}
private func updateTapAnimation() {
let timestamp = CACurrentMediaTime()
let deltaTime = min(timestamp - self.previousTapAnimationTimestamp, 1.0 / 60.0)
self.previousTapAnimationTimestamp = timestamp
let decelerationRate: CGFloat = 0.98
let lastTapDeltaTime = max(0.0, timestamp - self.previousTapTimestamp)
let tapAnimationTargetValue: CGFloat
if self.tapCounterView != nil {
tapAnimationTargetValue = 1.0 * CGFloat(pow(Double(decelerationRate), 1200.0 * lastTapDeltaTime))
} else {
tapAnimationTargetValue = 0.0
}
let advancementFraction = deltaTime * UIView.animationDurationFactor() * 120.0 / 60.0
self.tapAnimationValue = self.tapAnimationValue * (1.0 - advancementFraction) + tapAnimationTargetValue * advancementFraction
if self.tapAnimationValue <= 0.001 && self.previousTapTimestamp + 2.0 < timestamp {
self.tapAnimationValue = 0.0
self.tapAnimationLink?.invalidate()
self.tapAnimationLink = nil
if let tapCounterView = self.tapCounterView {
self.tapCounterView = nil
tapCounterView.alpha = 0.0
tapCounterView.animateOut(completion: { [weak tapCounterView] in
tapCounterView?.removeFromSuperview()
})
}
}
let tapAnimationFactor = max(0.0, min(1.0, self.tapAnimationValue / 0.3))
let scaleValue: CGFloat = 1.0 + tapAnimationFactor * 0.5
self.buttonNode.layer.transform = CATransform3DMakeScale(scaleValue, scaleValue, 1.0)
}
fileprivate func apply(layout: Layout, animation: ListViewItemUpdateAnimation, arguments: ReactionButtonsAsyncLayoutContainer.Arguments) {
self.containerView.frame = CGRect(origin: CGPoint(), size: layout.size)
self.containerView.contentView.frame = CGRect(origin: CGPoint(), size: layout.size)
self.containerView.contentRect = CGRect(origin: CGPoint(), size: layout.size)
animation.animator.updateFrame(layer: self.buttonNode.layer, frame: CGRect(origin: CGPoint(), size: layout.size), completion: nil)
let buttonFrame = CGRect(origin: CGPoint(), size: layout.size)
animation.animator.updatePosition(layer: self.buttonNode.layer, position: buttonFrame.center, completion: nil)
animation.animator.updateBounds(layer: self.buttonNode.layer, bounds: CGRect(origin: CGPoint(), size: buttonFrame.size), completion: nil)
if case .stars = layout.spec.component.reaction.value {
let starsEffectLayer: StarsButtonEffectLayer
@ -1423,3 +1518,83 @@ public final class ReactionButtonsAsyncLayoutContainer {
)
}
}
private final class StarsReactionCounterView: UIView {
private let portalSource: PortalSourceView
private let label = ComponentView<Empty>()
private var count: Int
init(count: Int) {
self.count = count
let portalSource = PortalSourceView()
portalSource.needsGlobalPortal = true
self.portalSource = portalSource
super.init(frame: CGRect())
self.addSubview(portalSource)
portalSource.frame = CGRect(origin: CGPoint(x: -200.0, y: -200.0), size: CGSize(width: 400.0, height: 400.0))
self.update(transition: .immediate)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func animateIn() {
if let labelView = self.label.view {
labelView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.15)
labelView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
}
func animateOut(completion: @escaping () -> Void) {
if let labelView = self.label.view {
labelView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.15, removeOnCompletion: false)
labelView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in
completion()
})
} else {
completion()
}
}
func add() {
self.count += 1
self.update(transition: .easeInOut(duration: 0.15))
}
func update(transition: ComponentTransition) {
var items: [AnimatedTextComponent.Item] = []
items.append(AnimatedTextComponent.Item(id: AnyHashable(0), content: .text("+")))
items.append(AnimatedTextComponent.Item(id: AnyHashable(1), content: .number(self.count, minDigits: 1)))
let labelSize = self.label.update(
transition: transition,
component: AnyComponent(AnimatedTextComponent(
font: Font.with(size: 40.0, design: .round, weight: .bold),
color: .white,
items: items
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 200.0)
)
let labelFrame = CGRect(origin: CGPoint(x: floor((self.portalSource.bounds.width - labelSize.width) * 0.5), y: floor((self.portalSource.bounds.height - labelSize.height) * 0.5)), size: labelSize)
if let labelView = self.label.view {
if labelView.superview == nil {
self.portalSource.addSubview(labelView)
labelView.layer.shadowColor = UIColor.black.cgColor
labelView.layer.shadowOffset = CGSize(width: 0.0, height: 1.0)
labelView.layer.shadowOpacity = 0.45
labelView.layer.shadowRadius = 9.0
}
transition.setFrame(view: labelView, frame: labelFrame)
}
}
}

View File

@ -3329,6 +3329,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
}
let switchToInlineImmediately: Bool
var playAnimationInline = false
if let itemNode {
if itemNode.item.listAnimation.isVideoEmoji || itemNode.item.listAnimation.isVideoSticker || itemNode.item.listAnimation.isAnimatedSticker || itemNode.item.listAnimation.isStaticEmoji {
switch itemNode.item.reaction.rawValue {
@ -3337,7 +3338,8 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
case .custom:
switchToInlineImmediately = true
case .stars:
switchToInlineImmediately = false
switchToInlineImmediately = true
playAnimationInline = true
}
} else {
switchToInlineImmediately = false
@ -3345,6 +3347,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
} else {
switchToInlineImmediately = false
}
let _ = playAnimationInline
if let itemNode, !forceSmallEffectAnimation, !switchToInlineImmediately, !hideCenterAnimation {
if let targetView = targetView as? ReactionIconView, !isLarge {
@ -3382,6 +3385,8 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
var expandedSize: CGSize = selfTargetRect.size
if isLarge {
expandedSize = CGSize(width: 120.0, height: 120.0)
} else if case .stars = reaction.reaction.rawValue {
expandedSize = CGSize(width: 120.0, height: 120.0)
}
let expandedFrame = CGRect(origin: CGPoint(x: selfTargetRect.midX - expandedSize.width / 2.0, y: selfTargetRect.midY - expandedSize.height / 2.0), size: expandedSize)
@ -3390,6 +3395,8 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0
if isLarge && !forceSmallEffectAnimation {
effectFrame = expandedFrame.insetBy(dx: -expandedFrame.width * 0.5, dy: -expandedFrame.height * 0.5).offsetBy(dx: incomingMessage ? (expandedFrame.width - 50.0) : (-expandedFrame.width + 50.0), dy: 0.0)
} else if case .stars = reaction.reaction.rawValue {
effectFrame = expandedFrame.insetBy(dx: -expandedFrame.width * 0.5, dy: -expandedFrame.height * 0.5)
} else {
effectFrame = expandedFrame.insetBy(dx: -expandedSize.width, dy: -expandedSize.height)
}
@ -3419,6 +3426,8 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
var additionalAnimationResource: MediaResource?
if isLarge && !forceSmallEffectAnimation {
additionalAnimationResource = reaction.largeApplicationAnimation?.resource
} else if case .stars = reaction.reaction.rawValue {
additionalAnimationResource = reaction.largeApplicationAnimation?.resource ?? reaction.applicationAnimation?.resource
} else {
additionalAnimationResource = reaction.applicationAnimation?.resource
}
@ -3949,7 +3958,8 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
let starSourceScale = sourceFrame.width / starSize.width
let starDestinationScale = selfTargetRect.width / starSize.width
let keyframes = generateParabollicMotionKeyframes(from: selfSourceRect.center, to: expandedFrame.center, elevation: 40.0)
let elevation: CGFloat = min(selfSourceRect.center.y, expandedFrame.center.y) - selfSourceRect.center.y - 40.0
let keyframes = generateParabollicMotionKeyframes(from: selfSourceRect.center, to: expandedFrame.center, elevation: -elevation)
let scaleKeyframes = generateScaleKeyframes(from: starSourceScale, center: 1.0, to: starDestinationScale)
starView.layer.transform = CATransform3DMakeScale(starDestinationScale, starDestinationScale, 1.0)
transition.animateScaleWithKeyframes(layer: starView.layer, keyframes: scaleKeyframes)

View File

@ -56,11 +56,13 @@ protocol ReactionItemNode: ASDisplayNode {
private let lockedBackgroundImage: UIImage = generateFilledCircleImage(diameter: 16.0, color: .white)!.withRenderingMode(.alwaysTemplate)
private let lockedBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeLock"), color: .white)
private final class StarsReactionEffectLayer: SimpleLayer {
private final class StarsButtonEffectLayer: SimpleLayer {
let emitterLayer = CAEmitterLayer()
override init() {
super.init()
//self.backgroundColor = UIColor.lightGray.withAlphaComponent(0.2).cgColor
self.addSublayer(self.emitterLayer)
}
override init(layer: Any) {
@ -71,7 +73,45 @@ private final class StarsReactionEffectLayer: SimpleLayer {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
let color = UIColor(rgb: 0xffbe27)
let emitter = CAEmitterCell()
emitter.name = "emitter"
emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
emitter.birthRate = 25.0
emitter.lifetime = 2.0
emitter.velocity = 12.0
emitter.velocityRange = 3
emitter.scale = 0.1
emitter.scaleRange = 0.08
emitter.alphaRange = 0.1
emitter.emissionRange = .pi * 2.0
emitter.setValue(3.0, forKey: "mass")
emitter.setValue(2.0, forKey: "massRange")
let staticColors: [Any] = [
color.withAlphaComponent(0.0).cgColor,
color.cgColor,
color.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(size: CGSize) {
if self.emitterLayer.emitterCells == nil {
self.setup()
}
self.emitterLayer.emitterShape = .circle
self.emitterLayer.emitterSize = CGSize(width: size.width * 0.7, height: size.height * 0.7)
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)
}
}
@ -88,7 +128,7 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
let selectionTintView: UIView?
let selectionView: UIView?
private var starsEffectLayer: StarsReactionEffectLayer?
private var starsEffectLayer: StarsButtonEffectLayer?
private var animateInAnimationNode: AnimatedStickerNode?
private var staticAnimationPlaceholderView: UIImageView?
@ -151,7 +191,7 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
super.init()
if case .stars = item.reaction.rawValue {
let starsEffectLayer = StarsReactionEffectLayer()
let starsEffectLayer = StarsButtonEffectLayer()
self.starsEffectLayer = starsEffectLayer
self.layer.addSublayer(starsEffectLayer)
}

View File

@ -4,7 +4,7 @@ import Postbox
import SwiftSignalKit
private func generateStarsReactionFile(kind: Int, isAnimatedSticker: Bool) -> TelegramMediaFile {
let baseId: Int64 = 52343278047832950
let baseId: Int64 = 52343278047832950 + 10
let fileId = baseId + Int64(kind)
var attributes: [TelegramMediaFileAttribute] = []

View File

@ -198,6 +198,28 @@ public func sendStarsReactionsInteractively(account: Account, messageId: Message
|> ignoreValues
}
func cancelPendingSendStarsReactionInteractively(account: Account, messageId: MessageId) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Void in
transaction.setPendingMessageAction(type: .sendStarsReaction, id: messageId, action: nil)
transaction.updateMessage(messageId, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
var attributes = currentMessage.attributes
loop: for j in 0 ..< attributes.count {
if let _ = attributes[j] as? PendingStarsReactionsMessageAttribute {
attributes.remove(at: j)
break loop
}
}
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
|> ignoreValues
}
private enum RequestUpdateMessageReactionError {
case generic
}
@ -356,7 +378,7 @@ private func requestSendStarsReaction(postbox: Postbox, network: Network, stateM
}
private final class ManagedApplyPendingMessageReactionsActionsHelper {
var operationDisposables: [MessageId: Disposable] = [:]
var operationDisposables: [MessageId: (PendingMessageActionData, Disposable)] = [:]
func update(entries: [PendingMessageActionsEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
@ -365,23 +387,26 @@ private final class ManagedApplyPendingMessageReactionsActionsHelper {
var hasRunningOperationForPeerId = Set<PeerId>()
var validIds = Set<MessageId>()
for entry in entries {
if let current = self.operationDisposables[entry.id], !current.0.isEqual(to: entry.action) {
self.operationDisposables.removeValue(forKey: entry.id)
disposeOperations.append(current.1)
}
if !hasRunningOperationForPeerId.contains(entry.id.peerId) {
hasRunningOperationForPeerId.insert(entry.id.peerId)
validIds.insert(entry.id)
if self.operationDisposables[entry.id] == nil {
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.id] = disposable
}
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.id] = (entry.action, disposable)
}
}
var removeMergedIds: [MessageId] = []
for (id, disposable) in self.operationDisposables {
for (id, actionAndDisposable) in self.operationDisposables {
if !validIds.contains(id) {
removeMergedIds.append(id)
disposeOperations.append(disposable)
disposeOperations.append(actionAndDisposable.1)
}
}
@ -393,7 +418,7 @@ private final class ManagedApplyPendingMessageReactionsActionsHelper {
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values)
let disposables = Array(self.operationDisposables.values.map(\.1))
self.operationDisposables.removeAll()
return disposables
}

View File

@ -109,7 +109,13 @@ func _internal_peerSendAsAvailablePeers(accountPeerId: PeerId, network: Network,
return .single([])
}
if let channel = peer as? TelegramChannel, case .group = channel.info {
if let channel = peer as? TelegramChannel {
if case .group = channel.info {
} else if case let .broadcast(info) = channel.info {
if !info.flags.contains(.messagesShouldHaveProfiles) {
return .single([])
}
}
} else {
return .single([])
}

View File

@ -337,6 +337,10 @@ public extension TelegramEngine {
public func sendStarsReaction(id: EngineMessage.Id, count: Int) {
let _ = sendStarsReactionsInteractively(account: self.account, messageId: id, count: count).startStandalone()
}
public func cancelPendingSendStarsReaction(id: EngineMessage.Id) {
let _ = cancelPendingSendStarsReactionInteractively(account: self.account, messageId: id).startStandalone()
}
public func requestChatContextResults(botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal<RequestChatContextResultsResult?, RequestChatContextResultsError> {
return _internal_requestChatContextResults(account: self.account, botId: botId, peerId: peerId, query: query, location: location, offset: offset, incompleteResults: incompleteResults, staleCachedResults: staleCachedResults)

View File

@ -541,7 +541,7 @@ public enum SendBotPaymentFormError {
}
public enum SendBotPaymentResult {
case done(receiptMessageId: MessageId?)
case done(receiptMessageId: MessageId?, subscriptionPeerId: PeerId?)
case externalVerificationRequired(url: String)
}
@ -585,6 +585,17 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa
case let .paymentResult(updates):
account.stateManager.addUpdates(updates)
var receiptMessageId: MessageId?
switch source {
case .starsChatSubscription:
let chats = updates.chats.compactMap { parseTelegramGroupOrChannel(chat: $0) }
if let first = chats.first {
return .done(receiptMessageId: nil, subscriptionPeerId: first.id)
}
default:
break
}
for apiMessage in updates.messages {
if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: account.peerId, peerIsForum: false) {
for media in message.media {
@ -623,7 +634,7 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa
}
}
}
return .done(receiptMessageId: receiptMessageId)
return .done(receiptMessageId: receiptMessageId, subscriptionPeerId: nil)
case let .paymentVerificationNeeded(url):
return .externalVerificationRequired(url: url)
}

View File

@ -1086,9 +1086,22 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot
return account.network.request(Api.functions.payments.sendStarsForm(flags: flags, formId: formId, invoice: invoice))
|> map { result -> SendBotPaymentResult in
switch result {
case let .paymentResult(updates):
account.stateManager.addUpdates(updates)
switch source {
case .starsChatSubscription:
let chats = updates.chats.compactMap { parseTelegramGroupOrChannel(chat: $0) }
if let first = chats.first {
return .done(receiptMessageId: nil, subscriptionPeerId: first.id)
}
default:
break
}
var receiptMessageId: MessageId?
for apiMessage in updates.messages {
if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: account.peerId, peerIsForum: false) {
@ -1130,7 +1143,7 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot
}
}
}
return .done(receiptMessageId: receiptMessageId)
return .done(receiptMessageId: receiptMessageId, subscriptionPeerId: nil)
case let .paymentVerificationNeeded(url):
return .externalVerificationRequired(url: url)
}

View File

@ -344,6 +344,11 @@ public extension Message {
return false
}
} else if self.author?.id == accountPeerId {
if let channel = self.peers[self.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info {
if !info.flags.contains(.messagesShouldHaveProfiles) {
return true
}
}
return false
} else if self.flags.contains(.Incoming) {
return true

View File

@ -76,6 +76,8 @@ public final class AnimatedTextComponent: Component {
let delayNorm: CGFloat = 0.002
var firstDelayWidth: CGFloat?
var validKeys: [CharacterKey] = []
for item in component.items {
var itemText: [String] = []
@ -138,20 +140,32 @@ public final class AnimatedTextComponent: Component {
if characterTransition.animation.isImmediate {
characterComponentView.frame = characterFrame
} else {
var delayWidth: Double = 0.0
if let firstDelayWidth {
delayWidth = size.width - firstDelayWidth
} else {
firstDelayWidth = size.width
}
characterComponentView.bounds = CGRect(origin: CGPoint(), size: characterFrame.size)
let deltaPosition = CGPoint(x: characterFrame.midX - characterComponentView.frame.midX, y: characterFrame.midY - characterComponentView.frame.midY)
characterComponentView.center = characterFrame.center
characterComponentView.layer.animatePosition(from: CGPoint(x: -deltaPosition.x, y: -deltaPosition.y), to: CGPoint(), duration: 0.4, delay: delayNorm * size.width, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
characterComponentView.layer.animatePosition(from: CGPoint(x: -deltaPosition.x, y: -deltaPosition.y), to: CGPoint(), duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
}
characterTransition.setFrame(view: characterComponentView, frame: characterFrame)
if animateIn, !transition.animation.isImmediate {
characterComponentView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.4, delay: delayNorm * size.width, timingFunction: kCAMediaTimingFunctionSpring)
//characterComponentView.layer.animateSpring(from: (characterSize.height * 0.5) as NSNumber, to: 0.0 as NSNumber, keyPath: "position.y", duration: 0.5, additive: true)
characterComponentView.layer.animatePosition(from: CGPoint(x: 0.0, y: characterSize.height * 0.5), to: CGPoint(), duration: 0.4, delay: delayNorm * size.width, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
characterComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18, delay: delayNorm * size.width)
var delayWidth: Double = 0.0
if let firstDelayWidth {
delayWidth = size.width - firstDelayWidth
} else {
firstDelayWidth = size.width
}
characterComponentView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring)
characterComponentView.layer.animatePosition(from: CGPoint(x: 0.0, y: characterSize.height * 0.5), to: CGPoint(), duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
characterComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18, delay: delayNorm * delayWidth)
}
}
@ -160,6 +174,11 @@ public final class AnimatedTextComponent: Component {
}
}
let outScaleTransition: ComponentTransition = .spring(duration: 0.4)
let outAlphaTransition: ComponentTransition = .easeInOut(duration: 0.18)
var outFirstDelayWidth: CGFloat?
var removedKeys: [CharacterKey] = []
for (key, characterView) in self.characters {
if !validKeys.contains(key) {
@ -167,9 +186,16 @@ public final class AnimatedTextComponent: Component {
if let characterComponentView = characterView.view {
if !transition.animation.isImmediate {
characterComponentView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.4, delay: delayNorm * characterComponentView.frame.minX, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
characterComponentView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -characterComponentView.bounds.height * 0.4), duration: 0.4, delay: delayNorm * characterComponentView.frame.minX, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
characterComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, delay: delayNorm * characterComponentView.frame.minX, removeOnCompletion: false, completion: { [weak characterComponentView] _ in
var delayWidth: Double = 0.0
if let outFirstDelayWidth {
delayWidth = characterComponentView.frame.minX - outFirstDelayWidth
} else {
outFirstDelayWidth = characterComponentView.frame.minX
}
outScaleTransition.setScale(view: characterComponentView, scale: 0.01, delay: delayNorm * delayWidth)
outScaleTransition.setPosition(view: characterComponentView, position: CGPoint(x: characterComponentView.center.x, y: characterComponentView.center.y - characterComponentView.bounds.height * 0.4), delay: delayNorm * delayWidth)
outAlphaTransition.setAlpha(view: characterComponentView, alpha: 0.0, delay: delayNorm * delayWidth, completion: { [weak characterComponentView] _ in
characterComponentView?.removeFromSuperview()
})
} else {

View File

@ -1480,7 +1480,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
var effectiveAuthor: Peer?
let overrideEffectiveAuthor = false
var overrideEffectiveAuthor = false
var ignoreForward = false
var displayAuthorInfo: Bool
var ignoreNameHiding = false
@ -1551,13 +1551,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
//TODO:release
/*if let channel = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, info.flags.contains(.messagesShouldHaveProfiles) {
hasAvatar = true
if let authorSignatureAttribute = firstMessage.authorSignatureAttribute {
effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignatureAttribute.signature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignatureAttribute.signature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)
overrideEffectiveAuthor = true
if let channel = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, firstMessage.author?.id != channel.id {
if info.flags.contains(.messagesShouldHaveProfiles) {
var allowAuthor = incoming
overrideEffectiveAuthor = true
if let author = firstMessage.author, author is TelegramChannel, !incoming || item.presentationData.isPreview {
allowAuthor = true
@ -1573,7 +1570,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
displayAuthorInfo = false
}
}
}*/
}
if !peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) {
if peerId.isGroupOrChannel && effectiveAuthor != nil {
@ -1591,6 +1588,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
hasAvatar = incoming
} else if case .customChatContents = item.chatLocation {
hasAvatar = false
} else if overrideEffectiveAuthor {
hasAvatar = true
}
}
} else if incoming {

View File

@ -126,16 +126,24 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess
var authorTitle: String?
if let author = message.author as? TelegramUser {
if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
authorTitle = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder)
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, message.author?.id != channel.id, info.flags.contains(.messagesShouldHaveProfiles) {
//TODO:release
} else {
authorTitle = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder)
}
} else if let forwardInfo = message.forwardInfo, forwardInfo.sourceMessageId?.peerId.namespace == Namespaces.Peer.CloudChannel {
authorTitle = forwardInfo.authorSignature
}
} else {
if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
for attribute in message.attributes {
if let attribute = attribute as? AuthorSignatureMessageAttribute {
authorTitle = attribute.signature
break
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, message.author?.id != channel.id, info.flags.contains(.messagesShouldHaveProfiles) {
//TODO:release
} else {
for attribute in message.attributes {
if let attribute = attribute as? AuthorSignatureMessageAttribute {
authorTitle = attribute.signature
break
}
}
}
}

View File

@ -344,13 +344,13 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
if !hasActionMedia {
if !isBroadcastChannel {
hasAvatar = true
}/* else if let channel = message.peers[message.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, info.flags.contains(.messagesShouldHaveProfiles) {
} else if let channel = message.peers[message.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, message.author?.id != channel.id {
//TODO:release
hasAvatar = true
if let authorSignatureAttribute = message.authorSignatureAttribute {
effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignatureAttribute.signature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignatureAttribute.signature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)
if info.flags.contains(.messagesShouldHaveProfiles) {
hasAvatar = true
effectiveAuthor = message.author
}
}*/
}
}
if hasAvatar {

View File

@ -56,7 +56,7 @@ private final class BalanceComponent: CombinedComponent {
static var body: Body {
let title = Child(MultilineTextComponent.self)
let balance = Child(MultilineTextComponent.self)
let icon = Child(EmojiStatusComponent.self)
let icon = Child(BundleIconComponent.self)
return { context in
var size = CGSize(width: 0.0, height: 0.0)
@ -89,19 +89,9 @@ private final class BalanceComponent: CombinedComponent {
let iconSize = CGSize(width: 18.0, height: 18.0)
let icon = icon.update(
component: EmojiStatusComponent(
context: context.component.context,
animationCache: context.component.context.animationCache,
animationRenderer: context.component.context.animationRenderer,
content: .animation(
content: .customEmoji(fileId: MessageReaction.starsReactionId), //TODO:release
size: iconSize,
placeholderColor: .gray,
themeColor: nil,
loopMode: .count(0)
),
isVisibleForAnimations: true,
action: nil
component: BundleIconComponent(
name: "Premium/Stars/StarLarge",
tintColor: nil
),
availableSize: iconSize,
transition: context.transition
@ -127,7 +117,7 @@ private final class BalanceComponent: CombinedComponent {
)
context.add(
icon.position(
icon.size.centered(in: CGRect(origin: CGPoint(x: 0.0, y: title.size.height + titleSpacing), size: icon.size)).center
icon.size.centered(in: CGRect(origin: CGPoint(x: -1.0, y: title.size.height + titleSpacing), size: icon.size)).center
)
)
@ -713,16 +703,10 @@ private final class SliderBackgroundComponent: Component {
topForegroundTextView.bounds = CGRect(origin: CGPoint(), size: topTextFrame.size)
topBackgroundTextView.bounds = CGRect(origin: CGPoint(), size: topTextFrame.size)
topForegroundTextView.isHidden = component.topCutoff == nil || topTextFrame.maxX >= availableSize.width - 4.0
topBackgroundTextView.isHidden = component.topCutoff == nil || topTextFrame.maxX >= availableSize.width - 4.0
}
if component.topCutoff == nil {
self.topForegroundLine.isHidden = true
self.topBackgroundLine.isHidden = true
} else {
self.topForegroundLine.isHidden = false
self.topBackgroundLine.isHidden = false
topForegroundTextView.isHidden = component.topCutoff == nil || topTextFrame.minX <= 10.0 || topTextFrame.maxX >= availableSize.width - 4.0
topBackgroundTextView.isHidden = topForegroundTextView.isHidden
self.topBackgroundLine.isHidden = topForegroundTextView.isHidden
self.topForegroundLine.isHidden = topForegroundTextView.isHidden
}
return availableSize
@ -743,20 +727,26 @@ private final class ChatSendStarsScreenComponent: Component {
let context: AccountContext
let peer: EnginePeer
let maxAmount: Int
let balance: Int64?
let currentSentAmount: Int?
let topPeers: [ChatSendStarsScreen.TopPeer]
let completion: (Int64, Bool, ChatSendStarsScreen.TransitionOut) -> Void
init(
context: AccountContext,
peer: EnginePeer,
maxAmount: Int,
balance: Int64?,
currentSentAmount: Int?,
topPeers: [ChatSendStarsScreen.TopPeer],
completion: @escaping (Int64, Bool, ChatSendStarsScreen.TransitionOut) -> Void
) {
self.context = context
self.peer = peer
self.maxAmount = maxAmount
self.balance = balance
self.currentSentAmount = currentSentAmount
self.topPeers = topPeers
self.completion = completion
}
@ -768,9 +758,15 @@ private final class ChatSendStarsScreenComponent: Component {
if lhs.peer != rhs.peer {
return false
}
if lhs.maxAmount != rhs.maxAmount {
return false
}
if lhs.balance != rhs.balance {
return false
}
if lhs.currentSentAmount != rhs.currentSentAmount {
return false
}
if lhs.topPeers != rhs.topPeers {
return false
}
@ -1005,7 +1001,7 @@ private final class ChatSendStarsScreenComponent: Component {
let sideInset: CGFloat = 16.0
if self.component == nil {
self.amount = 1
self.amount = 50
}
self.component = component
@ -1034,21 +1030,21 @@ private final class ChatSendStarsScreenComponent: Component {
let sliderSize = self.slider.update(
transition: transition,
component: AnyComponent(SliderComponent(
valueCount: 1000,
value: 0,
valueCount: component.maxAmount,
value: Int(self.amount),
markPositions: false,
trackBackgroundColor: .clear,
trackForegroundColor: .clear,
knobSize: 26.0,
knobColor: .white,
valueUpdated: { [weak self] value in
guard let self else {
guard let self, let component = self.component else {
return
}
self.amount = 1 + Int64(value)
self.state?.updated(transition: .immediate)
let sliderValue = Float(value) / 1000.0
let sliderValue = Float(value) / Float(component.maxAmount)
let currentTimestamp = CACurrentMediaTime()
if let previousTimestamp {
@ -1102,13 +1098,13 @@ private final class ChatSendStarsScreenComponent: Component {
let sliderFrame = CGRect(origin: CGPoint(x: sliderInset, y: contentHeight + 127.0), size: sliderSize)
let sliderBackgroundFrame = CGRect(origin: CGPoint(x: sliderFrame.minX - 8.0, y: sliderFrame.minY + 7.0), size: CGSize(width: sliderFrame.width + 16.0, height: sliderFrame.height - 14.0))
let progressFraction: CGFloat = CGFloat(self.amount) / CGFloat(1000 - 1)
let progressFraction: CGFloat = CGFloat(self.amount) / CGFloat(component.maxAmount - 1)
let topCount = component.topPeers.max(by: { $0.count < $1.count })?.count
var topCutoffFraction: CGFloat?
if let topCount {
let topCutoffFractionValue = CGFloat(topCount) / CGFloat(1000 - 1)
let topCutoffFractionValue = CGFloat(topCount) / CGFloat(component.maxAmount - 1)
topCutoffFraction = topCutoffFractionValue
let isPastCutoff = progressFraction >= topCutoffFractionValue
@ -1271,8 +1267,13 @@ private final class ChatSendStarsScreenComponent: Component {
contentHeight += 56.0
contentHeight += 8.0
let text = "Choose how many stars you want to send to **\(component.peer.debugDisplayTitle)** to support this post."
let text: String
if let currentSentAmount = component.currentSentAmount {
text = "You sent **\(currentSentAmount)** stars to support this post."
} else {
text = "Choose how many stars you want to send to **\(component.peer.debugDisplayTitle)** to support this post."
}
let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)
let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor)
@ -1469,6 +1470,32 @@ private final class ChatSendStarsScreenComponent: Component {
guard let self, let component = self.component else {
return
}
guard let balance = component.balance else {
return
}
if balance < self.amount {
let _ = (component.context.engine.payments.starsTopUpOptions()
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] options in
guard let self, let component = self.component else {
return
}
guard let starsContext = component.context.starsContext else {
return
}
let purchaseScreen = component.context.sharedContext.makeStarsPurchaseScreen(context: component.context, starsContext: starsContext, options: options, purpose: .transfer(peerId: component.peer.id, requiredStars: self.amount), completion: { result in
let _ = result
//TODO:release
})
self.environment?.controller()?.push(purchaseScreen)
self.environment?.controller()?.dismiss()
})
return
}
guard let badgeView = self.badge.view as? BadgeComponent.View else {
return
}
@ -1478,6 +1505,7 @@ private final class ChatSendStarsScreenComponent: Component {
} else {
isBecomingTop = true
}
component.completion(
self.amount,
isBecomingTop,
@ -1579,15 +1607,18 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
public final class InitialData {
fileprivate let peer: EnginePeer
fileprivate let balance: Int64?
fileprivate let currentSentAmount: Int?
fileprivate let topPeers: [ChatSendStarsScreen.TopPeer]
fileprivate init(
peer: EnginePeer,
balance: Int64?,
currentSentAmount: Int?,
topPeers: [ChatSendStarsScreen.TopPeer]
) {
self.peer = peer
self.balance = balance
self.currentSentAmount = currentSentAmount
self.topPeers = topPeers
}
}
@ -1629,10 +1660,17 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
public init(context: AccountContext, initialData: InitialData, completion: @escaping (Int64, Bool, TransitionOut) -> Void) {
self.context = context
var maxAmount = 2500
if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["stars_paid_reaction_amount_max"] as? Double {
maxAmount = Int(value)
}
super.init(context: context, component: ChatSendStarsScreenComponent(
context: context,
peer: initialData.peer,
maxAmount: maxAmount,
balance: initialData.balance,
currentSentAmount: initialData.currentSentAmount,
topPeers: initialData.topPeers,
completion: completion
), navigationBarAppearance: .none)
@ -1672,6 +1710,16 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
balance = .single(nil)
}
var currentSentAmount: Int?
if let myPeer = topPeers.first(where: { $0.isMy }) {
currentSentAmount = Int(myPeer.count)
}
var topPeers = topPeers.sorted(by: { $0.count < $1.count })
if topPeers.count > 3 {
topPeers = Array(topPeers.prefix(3))
}
return combineLatest(
context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
@ -1688,6 +1736,7 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
return InitialData(
peer: peer,
balance: balance,
currentSentAmount: currentSentAmount,
topPeers: topPeers.compactMap { topPeer -> ChatSendStarsScreen.TopPeer? in
guard let topPeerValue = topPeerMap[topPeer.peerId] else {
return nil

View File

@ -124,7 +124,7 @@ private func rippleOffset(
}
if distance <= 60.0 {
rippleAmount = 0.4 * rippleAmount
rippleAmount = 0.3 * rippleAmount
}
// A vector of length `amplitude` that points away from position.

View File

@ -373,7 +373,8 @@ extension ChatControllerImpl {
}
}
let _ = sendStarsReactionsInteractively(account: self.context.account, messageId: message.id, count: 1).startStandalone()
self.context.engine.messages.sendStarsReaction(id: message.id, count: 1)
self.displayOrUpdateSendStarsUndo(messageId: message.id, count: 1)
} else {
let chosenReaction: MessageReaction.Reaction = chosenUpdatedReaction.reaction

View File

@ -613,6 +613,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var messageComposeController: MFMessageComposeViewController?
weak var currentSendStarsUndoController: UndoOverlayController?
var currentSendStarsUndoMessageId: EngineMessage.Id?
var currentSendStarsUndoCount: Int = 0
public var alwaysShowSearchResultsAsList: Bool = false {
didSet {
self.presentationInterfaceState = self.presentationInterfaceState.updatedDisplayHistoryFilterAsList(self.alwaysShowSearchResultsAsList)
@ -1677,24 +1681,40 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
})
let _ = sendStarsReactionsInteractively(account: strongSelf.context.account, messageId: message.id, count: 1).startStandalone()
if !"".isEmpty {
let _ = (strongSelf.context.engine.stickers.resolveInlineStickers(fileIds: [MessageReaction.starsReactionId])
|> deliverOnMainQueue).start(next: { [weak strongSelf, weak itemNode] files in
guard let strongSelf, let file = files[MessageReaction.starsReactionId] else {
return
}
//TODO:localize
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .starsSent(context: strongSelf.context, file: file, amount: 1, title: "Star Sent", text: "Long tap on {star} to select custom quantity of stars."), elevatedLayout: false, action: { _ in
return false
}), in: .current)
if let itemNode = itemNode, let targetView = itemNode.targetReactionView(value: chosenReaction) {
strongSelf.chatDisplayNode.wrappingNode.triggerRipple(at: targetView.convert(targetView.bounds.center, to: strongSelf.chatDisplayNode.view))
}
})
guard let starsContext = strongSelf.context.starsContext else {
return
}
let _ = (starsContext.state
|> take(1)
|> deliverOnMainQueue).start(next: { [weak strongSelf] state in
guard let strongSelf, let balance = state?.balance else {
return
}
if balance < 1 {
let _ = (strongSelf.context.engine.payments.starsTopUpOptions()
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak strongSelf] options in
guard let strongSelf, let peerId = strongSelf.chatLocation.peerId else {
return
}
guard let starsContext = strongSelf.context.starsContext else {
return
}
let purchaseScreen = strongSelf.context.sharedContext.makeStarsPurchaseScreen(context: strongSelf.context, starsContext: starsContext, options: options, purpose: .transfer(peerId: peerId, requiredStars: 1), completion: { result in
let _ = result
//TODO:release
})
strongSelf.push(purchaseScreen)
})
return
}
strongSelf.context.engine.messages.sendStarsReaction(id: message.id, count: 1)
strongSelf.displayOrUpdateSendStarsUndo(messageId: message.id, count: 1)
})
} else {
var removedReaction: MessageReaction.Reaction?
var messageAlreadyHasThisReaction = false

View File

@ -19,6 +19,7 @@ import ChatSendStarsScreen
import ChatMessageItemCommon
import ChatMessageItemView
import ReactionSelectionNode
import AnimatedTextComponent
extension ChatControllerImpl {
func presentTagPremiumPaywall() {
@ -171,6 +172,7 @@ extension ChatControllerImpl {
guard let self, let initialData else {
return
}
HapticFeedback().tap()
self.push(ChatSendStarsScreen(context: self.context, initialData: initialData, completion: { [weak self] amount, isBecomingTop, transitionOut in
guard let self, amount > 0 else {
return
@ -256,26 +258,11 @@ extension ChatControllerImpl {
}
}
//let _ = self.context.engine.messages.sendStarsReaction(id: message.id, count: Int(amount))
#if !DEBUG
let _ = self.context.engine.messages.sendStarsReaction(id: message.id, count: Int(amount))
#endif
let _ = (self.context.engine.stickers.resolveInlineStickers(fileIds: [MessageReaction.starsReactionId])
|> deliverOnMainQueue).start(next: { [weak self] files in
guard let self, let file = files[MessageReaction.starsReactionId] else {
return
}
//TODO:localize
let title: String
if amount == 1 {
title = "Star Sent"
} else {
title = "\(amount) Stars Sent"
}
self.present(UndoOverlayController(presentationData: self.presentationData, content: .starsSent(context: self.context, file: file, amount: amount, title: title, text: nil), elevatedLayout: false, action: { _ in
return false
}), in: .current)
})
self.displayOrUpdateSendStarsUndo(messageId: message.id, count: Int(amount))
}))
})
@ -477,4 +464,50 @@ extension ChatControllerImpl {
})
}
}
func displayOrUpdateSendStarsUndo(messageId: EngineMessage.Id, count: Int) {
if self.currentSendStarsUndoMessageId != messageId {
if let current = self.currentSendStarsUndoController {
self.currentSendStarsUndoController = nil
current.dismiss()
}
}
if let _ = self.currentSendStarsUndoController {
self.currentSendStarsUndoCount += count
} else {
self.currentSendStarsUndoCount = count
}
//TODO:localize
let title: String
if self.currentSendStarsUndoCount == 1 {
title = "Star sent!"
} else {
title = "Stars sent!"
}
var textItems: [AnimatedTextComponent.Item] = []
textItems.append(AnimatedTextComponent.Item(id: AnyHashable(0), isUnbreakable: true, content: .text("You have reacted with ")))
textItems.append(AnimatedTextComponent.Item(id: AnyHashable(1), content: .number(self.currentSendStarsUndoCount, minDigits: 1)))
textItems.append(AnimatedTextComponent.Item(id: AnyHashable(2), isUnbreakable: true, content: .text(self.currentSendStarsUndoCount == 1 ? " star." : " stars.")))
self.currentSendStarsUndoMessageId = messageId
//TODO:localize
if let current = self.currentSendStarsUndoController {
current.content = .starsSent(context: self.context, title: title, text: textItems)
} else {
let controller = UndoOverlayController(presentationData: self.presentationData, content: .starsSent(context: self.context, title: title, text: textItems), elevatedLayout: false, position: .top, action: { [weak self] action in
guard let self else {
return false
}
if case .undo = action {
self.context.engine.messages.cancelPendingSendStarsReaction(id: messageId)
}
return false
})
self.currentSendStarsUndoController = controller
self.present(controller, in: .current)
}
}
}

View File

@ -30,6 +30,9 @@ swift_library(
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/Components/ComponentDisplayAdapters",
],
visibility = [
"//visibility:public",

View File

@ -5,6 +5,7 @@ import TelegramPresentationData
import TelegramCore
import AccountContext
import ComponentFlow
import AnimatedTextComponent
public enum UndoOverlayContent {
case removedChat(title: String, text: String?)
@ -39,7 +40,7 @@ public enum UndoOverlayContent {
case copy(text: String)
case mediaSaved(text: String)
case paymentSent(currencyValue: String, itemTitle: String)
case starsSent(context: AccountContext, file: TelegramMediaFile, amount: Int64, title: String, text: String?)
case starsSent(context: AccountContext, title: String, text: [AnimatedTextComponent.Item])
case inviteRequestSent(title: String, text: String)
case image(image: UIImage, title: String?, text: String, round: Bool, undoText: String?)
case notificationSoundAdded(title: String, text: String, action: (() -> Void)?)

View File

@ -20,6 +20,9 @@ import AnimatedAvatarSetNode
import ComponentFlow
import EmojiStatusComponent
import TextNodeWithEntities
import BundleIconComponent
import AnimatedTextComponent
import ComponentDisplayAdapters
final class UndoOverlayControllerNode: ViewControllerTracingNode {
private let presentationData: PresentationData
@ -42,6 +45,8 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
private var emojiStatus: ComponentView<Empty>?
private let titleNode: ImmediateTextNode
private let textNode: ImmediateTextNodeWithEntities
private var textComponent: ComponentView<Empty>?
private var animatedTextItems: [AnimatedTextComponent.Item]?
private let buttonNode: HighlightTrackingButtonNode
private let undoButtonTextNode: ImmediateTextNode
private let undoButtonNode: HighlightTrackingButtonNode
@ -84,6 +89,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.timerTextNode.displaysAsynchronously = false
self.titleNode = ImmediateTextNode()
self.titleNode.layer.anchorPoint = CGPoint()
self.titleNode.displaysAsynchronously = false
self.titleNode.maximumNumberOfLines = 0
@ -380,32 +386,23 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.textNode.attributedText = string
displayUndo = false
self.originalRemainingSeconds = 5
case let .starsSent(context, file, _, title, text):
case let .starsSent(_, title, textItems):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = nil
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
let imageBoundingSize = CGSize(width: 34.0, height: 34.0)
let emojiStatus = ComponentView<Empty>()
self.emojiStatus = emojiStatus
let _ = emojiStatus.update(
transition: .immediate,
component: AnyComponent(EmojiStatusComponent(
context: context,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
content: .animation(
content: .file(file: file),
size: imageBoundingSize,
placeholderColor: UIColor(white: 1.0, alpha: 0.1),
themeColor: .white,
loopMode: .count(1)
),
isVisibleForAnimations: true,
useSharedAnimation: false,
action: nil
component: AnyComponent(BundleIconComponent(
name: "Premium/Stars/StarLarge",
tintColor: nil
)),
environment: {},
containerSize: imageBoundingSize
@ -413,34 +410,10 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.stickerImageSize = imageBoundingSize
if let text {
let formattedString = text
let string = NSMutableAttributedString(attributedString: NSAttributedString(string: formattedString, font: Font.regular(14.0), textColor: .white))
let starRange = (string.string as NSString).range(of: "{star}")
if starRange.location != NSNotFound {
string.replaceCharacters(in: starRange, with: "")
string.insert(NSAttributedString(string: ".", attributes: [
.font: Font.regular(14.0),
ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: MessageReaction.starsReactionId, file: file, custom: nil)
]), at: starRange.location)
}
self.textNode.attributedText = string
self.textNode.arguments = TextNodeWithEntities.Arguments(
context: context,
cache: context.animationCache,
renderer: context.animationRenderer,
placeholderColor: UIColor(white: 1.0, alpha: 0.1),
attemptSynchronous: false
)
self.textNode.visibility = true
}
self.animatedTextItems = textItems
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
displayUndo = false
self.originalRemainingSeconds = 3
displayUndo = true
self.originalRemainingSeconds = 4.5
isUserInteractionEnabled = true
case let .messagesUnpinned(title, text, undo, isHidden):
self.avatarNode = nil
@ -1485,6 +1458,8 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
var undoTextColor = self.presentationData.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0)
var transition: ContainedViewLayoutTransition = .immediate
switch content {
case let .info(title, text, _, _), let .universal(_, _, _, title, text, _, _):
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
@ -1516,12 +1491,19 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
}
self.textNode.attributedText = attributedText
case let .starsSent(_, title, textItems):
self.animatedTextItems = textItems
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
self.renewWithCurrentContent()
transition = .animated(duration: 0.1, curve: .easeInOut)
default:
break
}
if let validLayout = self.validLayout {
self.containerLayoutUpdated(layout: validLayout, transition: .immediate)
self.containerLayoutUpdated(layout: validLayout, transition: transition)
}
}
@ -1579,7 +1561,41 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
}
let titleSize = self.titleNode.updateLayout(CGSize(width: buttonMinX - 8.0 - leftInset - layout.safeInsets.left - leftMargin, height: .greatestFiniteMagnitude))
let textSize = self.textNode.updateLayout(CGSize(width: buttonMinX - 8.0 - leftInset - layout.safeInsets.left - leftMargin, height: .greatestFiniteMagnitude))
let maxTextSize = CGSize(width: buttonMinX - 8.0 - leftInset - layout.safeInsets.left - leftMargin, height: .greatestFiniteMagnitude)
let textSize: CGSize
if let animatedTextItems = self.animatedTextItems {
let textComponent: ComponentView<Empty>
if let current = self.textComponent {
textComponent = current
} else {
textComponent = ComponentView()
self.textComponent = textComponent
}
textSize = textComponent.update(
transition: ComponentTransition(transition),
component: AnyComponent(AnimatedTextComponent(
font: Font.regular(14.0),
color: .white,
items: animatedTextItems
)),
environment: {},
containerSize: maxTextSize
)
if let textComponentView = textComponent.view {
if textComponentView.superview == nil {
textComponentView.layer.anchorPoint = CGPoint()
self.panelWrapperNode.view.addSubview(textComponentView)
}
}
} else {
if let textComponentView = self.textComponent?.view {
self.textComponent = nil
textComponentView.removeFromSuperview()
}
textSize = self.textNode.updateLayout(maxTextSize)
}
if !titleSize.width.isZero {
contentHeight += titleSize.height + 1.0
@ -1630,8 +1646,17 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
}
let textContentOrigin = floor((contentHeight - textContentHeight) / 2.0)
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: textContentOrigin), size: titleSize))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: leftInset, y: textContentOrigin + textOffset), size: textSize))
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: textContentOrigin), size: titleSize)
transition.updatePosition(node: self.titleNode, position: titleFrame.origin)
self.titleNode.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
let textFrame = CGRect(origin: CGPoint(x: leftInset, y: textContentOrigin + textOffset), size: textSize)
if let textComponentView = self.textComponent?.view {
transition.updatePosition(layer: textComponentView.layer, position: textFrame.origin)
textComponentView.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
} else {
transition.updateFrame(node: self.textNode, frame: textFrame)
}
if let iconNode = self.iconNode {
let iconSize: CGSize