mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Star reactions
This commit is contained in:
parent
0986fc0352
commit
6e13876636
@ -25,6 +25,7 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||||
"//submodules/TextFormat:TextFormat",
|
"//submodules/TextFormat:TextFormat",
|
||||||
"//submodules/AppBundle",
|
"//submodules/AppBundle",
|
||||||
|
"//submodules/TelegramUI/Components/AnimatedTextComponent",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -15,6 +15,7 @@ import MultiAnimationRenderer
|
|||||||
import EmojiTextAttachmentView
|
import EmojiTextAttachmentView
|
||||||
import TextFormat
|
import TextFormat
|
||||||
import AppBundle
|
import AppBundle
|
||||||
|
import AnimatedTextComponent
|
||||||
|
|
||||||
private let tagImage: UIImage? = {
|
private let tagImage: UIImage? = {
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ReactionTagBackground"), color: .white)?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 15)
|
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 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 {
|
public var activateAfterCompletion: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
if self.activateAfterCompletion {
|
if self.activateAfterCompletion {
|
||||||
@ -931,13 +938,101 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
layout.spec.component.action(self, layout.spec.component.reaction.value, self.containerView)
|
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) {
|
fileprivate func apply(layout: Layout, animation: ListViewItemUpdateAnimation, arguments: ReactionButtonsAsyncLayoutContainer.Arguments) {
|
||||||
self.containerView.frame = CGRect(origin: CGPoint(), size: layout.size)
|
self.containerView.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||||
self.containerView.contentView.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)
|
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 {
|
if case .stars = layout.spec.component.reaction.value {
|
||||||
let starsEffectLayer: StarsButtonEffectLayer
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3329,6 +3329,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let switchToInlineImmediately: Bool
|
let switchToInlineImmediately: Bool
|
||||||
|
var playAnimationInline = false
|
||||||
if let itemNode {
|
if let itemNode {
|
||||||
if itemNode.item.listAnimation.isVideoEmoji || itemNode.item.listAnimation.isVideoSticker || itemNode.item.listAnimation.isAnimatedSticker || itemNode.item.listAnimation.isStaticEmoji {
|
if itemNode.item.listAnimation.isVideoEmoji || itemNode.item.listAnimation.isVideoSticker || itemNode.item.listAnimation.isAnimatedSticker || itemNode.item.listAnimation.isStaticEmoji {
|
||||||
switch itemNode.item.reaction.rawValue {
|
switch itemNode.item.reaction.rawValue {
|
||||||
@ -3337,7 +3338,8 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
|||||||
case .custom:
|
case .custom:
|
||||||
switchToInlineImmediately = true
|
switchToInlineImmediately = true
|
||||||
case .stars:
|
case .stars:
|
||||||
switchToInlineImmediately = false
|
switchToInlineImmediately = true
|
||||||
|
playAnimationInline = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switchToInlineImmediately = false
|
switchToInlineImmediately = false
|
||||||
@ -3345,6 +3347,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
|||||||
} else {
|
} else {
|
||||||
switchToInlineImmediately = false
|
switchToInlineImmediately = false
|
||||||
}
|
}
|
||||||
|
let _ = playAnimationInline
|
||||||
|
|
||||||
if let itemNode, !forceSmallEffectAnimation, !switchToInlineImmediately, !hideCenterAnimation {
|
if let itemNode, !forceSmallEffectAnimation, !switchToInlineImmediately, !hideCenterAnimation {
|
||||||
if let targetView = targetView as? ReactionIconView, !isLarge {
|
if let targetView = targetView as? ReactionIconView, !isLarge {
|
||||||
@ -3382,6 +3385,8 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
|||||||
var expandedSize: CGSize = selfTargetRect.size
|
var expandedSize: CGSize = selfTargetRect.size
|
||||||
if isLarge {
|
if isLarge {
|
||||||
expandedSize = CGSize(width: 120.0, height: 120.0)
|
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)
|
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
|
let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0
|
||||||
if isLarge && !forceSmallEffectAnimation {
|
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)
|
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 {
|
} else {
|
||||||
effectFrame = expandedFrame.insetBy(dx: -expandedSize.width, dy: -expandedSize.height)
|
effectFrame = expandedFrame.insetBy(dx: -expandedSize.width, dy: -expandedSize.height)
|
||||||
}
|
}
|
||||||
@ -3419,6 +3426,8 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
|||||||
var additionalAnimationResource: MediaResource?
|
var additionalAnimationResource: MediaResource?
|
||||||
if isLarge && !forceSmallEffectAnimation {
|
if isLarge && !forceSmallEffectAnimation {
|
||||||
additionalAnimationResource = reaction.largeApplicationAnimation?.resource
|
additionalAnimationResource = reaction.largeApplicationAnimation?.resource
|
||||||
|
} else if case .stars = reaction.reaction.rawValue {
|
||||||
|
additionalAnimationResource = reaction.largeApplicationAnimation?.resource ?? reaction.applicationAnimation?.resource
|
||||||
} else {
|
} else {
|
||||||
additionalAnimationResource = reaction.applicationAnimation?.resource
|
additionalAnimationResource = reaction.applicationAnimation?.resource
|
||||||
}
|
}
|
||||||
@ -3949,7 +3958,8 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
|||||||
let starSourceScale = sourceFrame.width / starSize.width
|
let starSourceScale = sourceFrame.width / starSize.width
|
||||||
let starDestinationScale = selfTargetRect.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)
|
let scaleKeyframes = generateScaleKeyframes(from: starSourceScale, center: 1.0, to: starDestinationScale)
|
||||||
starView.layer.transform = CATransform3DMakeScale(starDestinationScale, starDestinationScale, 1.0)
|
starView.layer.transform = CATransform3DMakeScale(starDestinationScale, starDestinationScale, 1.0)
|
||||||
transition.animateScaleWithKeyframes(layer: starView.layer, keyframes: scaleKeyframes)
|
transition.animateScaleWithKeyframes(layer: starView.layer, keyframes: scaleKeyframes)
|
||||||
|
@ -4,7 +4,7 @@ import Postbox
|
|||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
|
||||||
private func generateStarsReactionFile(kind: Int, isAnimatedSticker: Bool) -> TelegramMediaFile {
|
private func generateStarsReactionFile(kind: Int, isAnimatedSticker: Bool) -> TelegramMediaFile {
|
||||||
let baseId: Int64 = 52343278047832950
|
let baseId: Int64 = 52343278047832950 + 10
|
||||||
let fileId = baseId + Int64(kind)
|
let fileId = baseId + Int64(kind)
|
||||||
|
|
||||||
var attributes: [TelegramMediaFileAttribute] = []
|
var attributes: [TelegramMediaFileAttribute] = []
|
||||||
|
@ -356,7 +356,7 @@ private func requestSendStarsReaction(postbox: Postbox, network: Network, stateM
|
|||||||
}
|
}
|
||||||
|
|
||||||
private final class ManagedApplyPendingMessageReactionsActionsHelper {
|
private final class ManagedApplyPendingMessageReactionsActionsHelper {
|
||||||
var operationDisposables: [MessageId: Disposable] = [:]
|
var operationDisposables: [MessageId: (PendingMessageActionData, Disposable)] = [:]
|
||||||
|
|
||||||
func update(entries: [PendingMessageActionsEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) {
|
func update(entries: [PendingMessageActionsEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) {
|
||||||
var disposeOperations: [Disposable] = []
|
var disposeOperations: [Disposable] = []
|
||||||
@ -365,23 +365,26 @@ private final class ManagedApplyPendingMessageReactionsActionsHelper {
|
|||||||
var hasRunningOperationForPeerId = Set<PeerId>()
|
var hasRunningOperationForPeerId = Set<PeerId>()
|
||||||
var validIds = Set<MessageId>()
|
var validIds = Set<MessageId>()
|
||||||
for entry in entries {
|
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) {
|
if !hasRunningOperationForPeerId.contains(entry.id.peerId) {
|
||||||
hasRunningOperationForPeerId.insert(entry.id.peerId)
|
hasRunningOperationForPeerId.insert(entry.id.peerId)
|
||||||
validIds.insert(entry.id)
|
validIds.insert(entry.id)
|
||||||
|
|
||||||
if self.operationDisposables[entry.id] == nil {
|
let disposable = MetaDisposable()
|
||||||
let disposable = MetaDisposable()
|
beginOperations.append((entry, disposable))
|
||||||
beginOperations.append((entry, disposable))
|
self.operationDisposables[entry.id] = (entry.action, disposable)
|
||||||
self.operationDisposables[entry.id] = disposable
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var removeMergedIds: [MessageId] = []
|
var removeMergedIds: [MessageId] = []
|
||||||
for (id, disposable) in self.operationDisposables {
|
for (id, actionAndDisposable) in self.operationDisposables {
|
||||||
if !validIds.contains(id) {
|
if !validIds.contains(id) {
|
||||||
removeMergedIds.append(id)
|
removeMergedIds.append(id)
|
||||||
disposeOperations.append(disposable)
|
disposeOperations.append(actionAndDisposable.1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -393,7 +396,7 @@ private final class ManagedApplyPendingMessageReactionsActionsHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func reset() -> [Disposable] {
|
func reset() -> [Disposable] {
|
||||||
let disposables = Array(self.operationDisposables.values)
|
let disposables = Array(self.operationDisposables.values.map(\.1))
|
||||||
self.operationDisposables.removeAll()
|
self.operationDisposables.removeAll()
|
||||||
return disposables
|
return disposables
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,13 @@ func _internal_peerSendAsAvailablePeers(accountPeerId: PeerId, network: Network,
|
|||||||
return .single([])
|
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 {
|
} else {
|
||||||
return .single([])
|
return .single([])
|
||||||
}
|
}
|
||||||
|
@ -344,6 +344,11 @@ public extension Message {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
} else if self.author?.id == accountPeerId {
|
} 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
|
return false
|
||||||
} else if self.flags.contains(.Incoming) {
|
} else if self.flags.contains(.Incoming) {
|
||||||
return true
|
return true
|
||||||
|
@ -1480,7 +1480,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
}
|
}
|
||||||
|
|
||||||
var effectiveAuthor: Peer?
|
var effectiveAuthor: Peer?
|
||||||
let overrideEffectiveAuthor = false
|
var overrideEffectiveAuthor = false
|
||||||
var ignoreForward = false
|
var ignoreForward = false
|
||||||
var displayAuthorInfo: Bool
|
var displayAuthorInfo: Bool
|
||||||
var ignoreNameHiding = false
|
var ignoreNameHiding = false
|
||||||
@ -1551,13 +1551,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
}
|
}
|
||||||
|
|
||||||
//TODO:release
|
//TODO:release
|
||||||
/*if let channel = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, info.flags.contains(.messagesShouldHaveProfiles) {
|
if let channel = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, firstMessage.author?.id != channel.id {
|
||||||
hasAvatar = true
|
if info.flags.contains(.messagesShouldHaveProfiles) {
|
||||||
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
|
|
||||||
|
|
||||||
var allowAuthor = incoming
|
var allowAuthor = incoming
|
||||||
|
overrideEffectiveAuthor = true
|
||||||
|
|
||||||
if let author = firstMessage.author, author is TelegramChannel, !incoming || item.presentationData.isPreview {
|
if let author = firstMessage.author, author is TelegramChannel, !incoming || item.presentationData.isPreview {
|
||||||
allowAuthor = true
|
allowAuthor = true
|
||||||
@ -1573,7 +1570,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
displayAuthorInfo = false
|
displayAuthorInfo = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}*/
|
}
|
||||||
|
|
||||||
if !peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) {
|
if !peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) {
|
||||||
if peerId.isGroupOrChannel && effectiveAuthor != nil {
|
if peerId.isGroupOrChannel && effectiveAuthor != nil {
|
||||||
@ -1591,6 +1588,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
hasAvatar = incoming
|
hasAvatar = incoming
|
||||||
} else if case .customChatContents = item.chatLocation {
|
} else if case .customChatContents = item.chatLocation {
|
||||||
hasAvatar = false
|
hasAvatar = false
|
||||||
|
} else if overrideEffectiveAuthor {
|
||||||
|
hasAvatar = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if incoming {
|
} else if incoming {
|
||||||
|
@ -126,16 +126,24 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess
|
|||||||
var authorTitle: String?
|
var authorTitle: String?
|
||||||
if let author = message.author as? TelegramUser {
|
if let author = message.author as? TelegramUser {
|
||||||
if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
|
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 {
|
} else if let forwardInfo = message.forwardInfo, forwardInfo.sourceMessageId?.peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||||
authorTitle = forwardInfo.authorSignature
|
authorTitle = forwardInfo.authorSignature
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
|
if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
|
||||||
for attribute in message.attributes {
|
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) {
|
||||||
if let attribute = attribute as? AuthorSignatureMessageAttribute {
|
//TODO:release
|
||||||
authorTitle = attribute.signature
|
} else {
|
||||||
break
|
for attribute in message.attributes {
|
||||||
|
if let attribute = attribute as? AuthorSignatureMessageAttribute {
|
||||||
|
authorTitle = attribute.signature
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -344,13 +344,13 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
|
|||||||
if !hasActionMedia {
|
if !hasActionMedia {
|
||||||
if !isBroadcastChannel {
|
if !isBroadcastChannel {
|
||||||
hasAvatar = true
|
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
|
//TODO:release
|
||||||
hasAvatar = true
|
if info.flags.contains(.messagesShouldHaveProfiles) {
|
||||||
if let authorSignatureAttribute = message.authorSignatureAttribute {
|
hasAvatar = true
|
||||||
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)
|
effectiveAuthor = message.author
|
||||||
}
|
}
|
||||||
}*/
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasAvatar {
|
if hasAvatar {
|
||||||
|
@ -56,7 +56,7 @@ private final class BalanceComponent: CombinedComponent {
|
|||||||
static var body: Body {
|
static var body: Body {
|
||||||
let title = Child(MultilineTextComponent.self)
|
let title = Child(MultilineTextComponent.self)
|
||||||
let balance = Child(MultilineTextComponent.self)
|
let balance = Child(MultilineTextComponent.self)
|
||||||
let icon = Child(EmojiStatusComponent.self)
|
let icon = Child(BundleIconComponent.self)
|
||||||
|
|
||||||
return { context in
|
return { context in
|
||||||
var size = CGSize(width: 0.0, height: 0.0)
|
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 iconSize = CGSize(width: 18.0, height: 18.0)
|
||||||
let icon = icon.update(
|
let icon = icon.update(
|
||||||
component: EmojiStatusComponent(
|
component: BundleIconComponent(
|
||||||
context: context.component.context,
|
name: "Premium/Stars/StarLarge",
|
||||||
animationCache: context.component.context.animationCache,
|
tintColor: nil
|
||||||
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
|
|
||||||
),
|
),
|
||||||
availableSize: iconSize,
|
availableSize: iconSize,
|
||||||
transition: context.transition
|
transition: context.transition
|
||||||
@ -127,7 +117,7 @@ private final class BalanceComponent: CombinedComponent {
|
|||||||
)
|
)
|
||||||
context.add(
|
context.add(
|
||||||
icon.position(
|
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)
|
topForegroundTextView.bounds = CGRect(origin: CGPoint(), size: topTextFrame.size)
|
||||||
topBackgroundTextView.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
|
topForegroundTextView.isHidden = component.topCutoff == nil || topTextFrame.minX <= 10.0 || topTextFrame.maxX >= availableSize.width - 4.0
|
||||||
topBackgroundTextView.isHidden = component.topCutoff == nil || topTextFrame.maxX >= availableSize.width - 4.0
|
topBackgroundTextView.isHidden = topForegroundTextView.isHidden
|
||||||
}
|
self.topBackgroundLine.isHidden = topForegroundTextView.isHidden
|
||||||
|
self.topForegroundLine.isHidden = topForegroundTextView.isHidden
|
||||||
if component.topCutoff == nil {
|
|
||||||
self.topForegroundLine.isHidden = true
|
|
||||||
self.topBackgroundLine.isHidden = true
|
|
||||||
} else {
|
|
||||||
self.topForegroundLine.isHidden = false
|
|
||||||
self.topBackgroundLine.isHidden = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return availableSize
|
return availableSize
|
||||||
@ -743,20 +727,26 @@ private final class ChatSendStarsScreenComponent: Component {
|
|||||||
|
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let peer: EnginePeer
|
let peer: EnginePeer
|
||||||
|
let maxAmount: Int
|
||||||
let balance: Int64?
|
let balance: Int64?
|
||||||
|
let currentSentAmount: Int?
|
||||||
let topPeers: [ChatSendStarsScreen.TopPeer]
|
let topPeers: [ChatSendStarsScreen.TopPeer]
|
||||||
let completion: (Int64, Bool, ChatSendStarsScreen.TransitionOut) -> Void
|
let completion: (Int64, Bool, ChatSendStarsScreen.TransitionOut) -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
peer: EnginePeer,
|
peer: EnginePeer,
|
||||||
|
maxAmount: Int,
|
||||||
balance: Int64?,
|
balance: Int64?,
|
||||||
|
currentSentAmount: Int?,
|
||||||
topPeers: [ChatSendStarsScreen.TopPeer],
|
topPeers: [ChatSendStarsScreen.TopPeer],
|
||||||
completion: @escaping (Int64, Bool, ChatSendStarsScreen.TransitionOut) -> Void
|
completion: @escaping (Int64, Bool, ChatSendStarsScreen.TransitionOut) -> Void
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.peer = peer
|
self.peer = peer
|
||||||
|
self.maxAmount = maxAmount
|
||||||
self.balance = balance
|
self.balance = balance
|
||||||
|
self.currentSentAmount = currentSentAmount
|
||||||
self.topPeers = topPeers
|
self.topPeers = topPeers
|
||||||
self.completion = completion
|
self.completion = completion
|
||||||
}
|
}
|
||||||
@ -768,9 +758,15 @@ private final class ChatSendStarsScreenComponent: Component {
|
|||||||
if lhs.peer != rhs.peer {
|
if lhs.peer != rhs.peer {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.maxAmount != rhs.maxAmount {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.balance != rhs.balance {
|
if lhs.balance != rhs.balance {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.currentSentAmount != rhs.currentSentAmount {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.topPeers != rhs.topPeers {
|
if lhs.topPeers != rhs.topPeers {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -1005,7 +1001,7 @@ private final class ChatSendStarsScreenComponent: Component {
|
|||||||
let sideInset: CGFloat = 16.0
|
let sideInset: CGFloat = 16.0
|
||||||
|
|
||||||
if self.component == nil {
|
if self.component == nil {
|
||||||
self.amount = 1
|
self.amount = 50
|
||||||
}
|
}
|
||||||
|
|
||||||
self.component = component
|
self.component = component
|
||||||
@ -1034,21 +1030,21 @@ private final class ChatSendStarsScreenComponent: Component {
|
|||||||
let sliderSize = self.slider.update(
|
let sliderSize = self.slider.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(SliderComponent(
|
component: AnyComponent(SliderComponent(
|
||||||
valueCount: 1000,
|
valueCount: component.maxAmount,
|
||||||
value: 0,
|
value: Int(self.amount),
|
||||||
markPositions: false,
|
markPositions: false,
|
||||||
trackBackgroundColor: .clear,
|
trackBackgroundColor: .clear,
|
||||||
trackForegroundColor: .clear,
|
trackForegroundColor: .clear,
|
||||||
knobSize: 26.0,
|
knobSize: 26.0,
|
||||||
knobColor: .white,
|
knobColor: .white,
|
||||||
valueUpdated: { [weak self] value in
|
valueUpdated: { [weak self] value in
|
||||||
guard let self else {
|
guard let self, let component = self.component else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.amount = 1 + Int64(value)
|
self.amount = 1 + Int64(value)
|
||||||
self.state?.updated(transition: .immediate)
|
self.state?.updated(transition: .immediate)
|
||||||
|
|
||||||
let sliderValue = Float(value) / 1000.0
|
let sliderValue = Float(value) / Float(component.maxAmount)
|
||||||
let currentTimestamp = CACurrentMediaTime()
|
let currentTimestamp = CACurrentMediaTime()
|
||||||
|
|
||||||
if let previousTimestamp {
|
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 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 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
|
let topCount = component.topPeers.max(by: { $0.count < $1.count })?.count
|
||||||
|
|
||||||
var topCutoffFraction: CGFloat?
|
var topCutoffFraction: CGFloat?
|
||||||
if let topCount {
|
if let topCount {
|
||||||
let topCutoffFractionValue = CGFloat(topCount) / CGFloat(1000 - 1)
|
let topCutoffFractionValue = CGFloat(topCount) / CGFloat(component.maxAmount - 1)
|
||||||
topCutoffFraction = topCutoffFractionValue
|
topCutoffFraction = topCutoffFractionValue
|
||||||
|
|
||||||
let isPastCutoff = progressFraction >= topCutoffFractionValue
|
let isPastCutoff = progressFraction >= topCutoffFractionValue
|
||||||
@ -1271,8 +1267,13 @@ private final class ChatSendStarsScreenComponent: Component {
|
|||||||
|
|
||||||
contentHeight += 56.0
|
contentHeight += 56.0
|
||||||
contentHeight += 8.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 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)
|
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 {
|
guard let self, let component = self.component else {
|
||||||
return
|
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 {
|
guard let badgeView = self.badge.view as? BadgeComponent.View else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1478,6 +1505,7 @@ private final class ChatSendStarsScreenComponent: Component {
|
|||||||
} else {
|
} else {
|
||||||
isBecomingTop = true
|
isBecomingTop = true
|
||||||
}
|
}
|
||||||
|
|
||||||
component.completion(
|
component.completion(
|
||||||
self.amount,
|
self.amount,
|
||||||
isBecomingTop,
|
isBecomingTop,
|
||||||
@ -1579,15 +1607,18 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
|
|||||||
public final class InitialData {
|
public final class InitialData {
|
||||||
fileprivate let peer: EnginePeer
|
fileprivate let peer: EnginePeer
|
||||||
fileprivate let balance: Int64?
|
fileprivate let balance: Int64?
|
||||||
|
fileprivate let currentSentAmount: Int?
|
||||||
fileprivate let topPeers: [ChatSendStarsScreen.TopPeer]
|
fileprivate let topPeers: [ChatSendStarsScreen.TopPeer]
|
||||||
|
|
||||||
fileprivate init(
|
fileprivate init(
|
||||||
peer: EnginePeer,
|
peer: EnginePeer,
|
||||||
balance: Int64?,
|
balance: Int64?,
|
||||||
|
currentSentAmount: Int?,
|
||||||
topPeers: [ChatSendStarsScreen.TopPeer]
|
topPeers: [ChatSendStarsScreen.TopPeer]
|
||||||
) {
|
) {
|
||||||
self.peer = peer
|
self.peer = peer
|
||||||
self.balance = balance
|
self.balance = balance
|
||||||
|
self.currentSentAmount = currentSentAmount
|
||||||
self.topPeers = topPeers
|
self.topPeers = topPeers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1629,10 +1660,17 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
|
|||||||
public init(context: AccountContext, initialData: InitialData, completion: @escaping (Int64, Bool, TransitionOut) -> Void) {
|
public init(context: AccountContext, initialData: InitialData, completion: @escaping (Int64, Bool, TransitionOut) -> Void) {
|
||||||
self.context = context
|
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(
|
super.init(context: context, component: ChatSendStarsScreenComponent(
|
||||||
context: context,
|
context: context,
|
||||||
peer: initialData.peer,
|
peer: initialData.peer,
|
||||||
|
maxAmount: maxAmount,
|
||||||
balance: initialData.balance,
|
balance: initialData.balance,
|
||||||
|
currentSentAmount: initialData.currentSentAmount,
|
||||||
topPeers: initialData.topPeers,
|
topPeers: initialData.topPeers,
|
||||||
completion: completion
|
completion: completion
|
||||||
), navigationBarAppearance: .none)
|
), navigationBarAppearance: .none)
|
||||||
@ -1672,6 +1710,16 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
|
|||||||
balance = .single(nil)
|
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(
|
return combineLatest(
|
||||||
context.engine.data.get(
|
context.engine.data.get(
|
||||||
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
|
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
|
||||||
@ -1688,6 +1736,7 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
|
|||||||
return InitialData(
|
return InitialData(
|
||||||
peer: peer,
|
peer: peer,
|
||||||
balance: balance,
|
balance: balance,
|
||||||
|
currentSentAmount: currentSentAmount,
|
||||||
topPeers: topPeers.compactMap { topPeer -> ChatSendStarsScreen.TopPeer? in
|
topPeers: topPeers.compactMap { topPeer -> ChatSendStarsScreen.TopPeer? in
|
||||||
guard let topPeerValue = topPeerMap[topPeer.peerId] else {
|
guard let topPeerValue = topPeerMap[topPeer.peerId] else {
|
||||||
return nil
|
return nil
|
||||||
|
@ -124,7 +124,7 @@ private func rippleOffset(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if distance <= 60.0 {
|
if distance <= 60.0 {
|
||||||
rippleAmount = 0.4 * rippleAmount
|
rippleAmount = 0.3 * rippleAmount
|
||||||
}
|
}
|
||||||
|
|
||||||
// A vector of length `amplitude` that points away from position.
|
// A vector of length `amplitude` that points away from position.
|
||||||
|
Binary file not shown.
@ -373,7 +373,7 @@ extension ChatControllerImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = sendStarsReactionsInteractively(account: self.context.account, messageId: message.id, count: 1).startStandalone()
|
self.context.engine.messages.sendStarsReaction(id: message.id, count: 1)
|
||||||
} else {
|
} else {
|
||||||
let chosenReaction: MessageReaction.Reaction = chosenUpdatedReaction.reaction
|
let chosenReaction: MessageReaction.Reaction = chosenUpdatedReaction.reaction
|
||||||
|
|
||||||
|
@ -1677,24 +1677,56 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let _ = sendStarsReactionsInteractively(account: strongSelf.context.account, messageId: message.id, count: 1).startStandalone()
|
guard let starsContext = strongSelf.context.starsContext else {
|
||||||
|
return
|
||||||
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))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
var removedReaction: MessageReaction.Reaction?
|
var removedReaction: MessageReaction.Reaction?
|
||||||
var messageAlreadyHasThisReaction = false
|
var messageAlreadyHasThisReaction = false
|
||||||
|
@ -171,6 +171,7 @@ extension ChatControllerImpl {
|
|||||||
guard let self, let initialData else {
|
guard let self, let initialData else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
HapticFeedback().tap()
|
||||||
self.push(ChatSendStarsScreen(context: self.context, initialData: initialData, completion: { [weak self] amount, isBecomingTop, transitionOut in
|
self.push(ChatSendStarsScreen(context: self.context, initialData: initialData, completion: { [weak self] amount, isBecomingTop, transitionOut in
|
||||||
guard let self, amount > 0 else {
|
guard let self, amount > 0 else {
|
||||||
return
|
return
|
||||||
@ -256,7 +257,9 @@ 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])
|
let _ = (self.context.engine.stickers.resolveInlineStickers(fileIds: [MessageReaction.starsReactionId])
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] files in
|
|> deliverOnMainQueue).start(next: { [weak self] files in
|
||||||
|
Loading…
x
Reference in New Issue
Block a user