From 6e138766362bc3ff84136f8b04ba02adfb8082e7 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 9 Aug 2024 16:55:53 +0400 Subject: [PATCH] Star reactions --- .../ReactionButtonListComponent/BUILD | 1 + .../Sources/ReactionButtonListComponent.swift | 177 +++++++++++++++++- .../Sources/ReactionContextNode.swift | 14 +- .../Sources/State/AvailableReactions.swift | 2 +- .../Sources/State/MessageReactions.swift | 21 ++- .../TelegramEngine/Messages/SendAsPeers.swift | 8 +- .../Sources/Utils/MessageUtils.swift | 5 + .../Sources/ChatMessageBubbleItemNode.swift | 15 +- .../StringForMessageTimestampStatus.swift | 18 +- .../Sources/ChatMessageItemImpl.swift | 10 +- .../Sources/ChatSendStarsScreen.swift | 117 ++++++++---- .../SpaceWarpView/Sources/SpaceWarpView.swift | 2 +- .../star_up/star_reaction_effect.tgs | Bin 22618 -> 8420 bytes ...ChatControllerOpenMessageContextMenu.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 66 +++++-- ...rollerOpenMessageReactionContextMenu.swift | 5 +- 16 files changed, 377 insertions(+), 86 deletions(-) diff --git a/submodules/Components/ReactionButtonListComponent/BUILD b/submodules/Components/ReactionButtonListComponent/BUILD index f8167464d8..19425cd17d 100644 --- a/submodules/Components/ReactionButtonListComponent/BUILD +++ b/submodules/Components/ReactionButtonListComponent/BUILD @@ -25,6 +25,7 @@ swift_library( "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TextFormat:TextFormat", "//submodules/AppBundle", + "//submodules/TelegramUI/Components/AnimatedTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index 3efc953198..b640158a24 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -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() + + 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) + } + } +} diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 1b7765b126..c8ae04d025 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -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) diff --git a/submodules/TelegramCore/Sources/State/AvailableReactions.swift b/submodules/TelegramCore/Sources/State/AvailableReactions.swift index 1fe767cb95..d0a4f149a0 100644 --- a/submodules/TelegramCore/Sources/State/AvailableReactions.swift +++ b/submodules/TelegramCore/Sources/State/AvailableReactions.swift @@ -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] = [] diff --git a/submodules/TelegramCore/Sources/State/MessageReactions.swift b/submodules/TelegramCore/Sources/State/MessageReactions.swift index f1703195a6..44613ed604 100644 --- a/submodules/TelegramCore/Sources/State/MessageReactions.swift +++ b/submodules/TelegramCore/Sources/State/MessageReactions.swift @@ -356,7 +356,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 +365,26 @@ private final class ManagedApplyPendingMessageReactionsActionsHelper { var hasRunningOperationForPeerId = Set() var validIds = Set() 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 +396,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 } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift index b35b4569ae..d50eb68079 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift @@ -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([]) } diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index 9cbd7eaa73..58903e0d0e 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 5ed325f2a3..2f6e0d90d3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift index ef3ac7d6ff..107fa3575c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift @@ -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 + } } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index 507bb11f17..9a4c368005 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index 37000034f2..fd122e068e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -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 diff --git a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift index 48f18d5f66..e36ad4b1fe 100644 --- a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift +++ b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift @@ -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. diff --git a/submodules/TelegramUI/Resources/Animations/star_up/star_reaction_effect.tgs b/submodules/TelegramUI/Resources/Animations/star_up/star_reaction_effect.tgs index f31897de51b445d2be1e18d1bbdcfea0df7385a4..0b87a225a4f0cb162cc29de04ca74261253d11dc 100644 GIT binary patch literal 8420 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 literal 22618 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 Tc+M^T_|N|jWg!; 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) + + 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 { var removedReaction: MessageReaction.Reaction? var messageAlreadyHasThisReaction = false diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift index 090fa6bdfc..b18d4fd139 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift @@ -171,6 +171,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,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]) |> deliverOnMainQueue).start(next: { [weak self] files in