diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 74e1bc2d72..73675e12e4 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -2671,6 +2671,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { var selfTargetBounds = targetView.bounds if case .builtin = itemNode.item.reaction.rawValue { selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5) + } else if case .stars = itemNode.item.reaction.rawValue { + selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5) } let selfTargetRect = self.view.convert(selfTargetBounds, from: targetView) @@ -3775,6 +3777,308 @@ public final class StandaloneReactionAnimation: ASDisplayNode { itemNode.layer.animateScale(from: 1.0, to: (targetSnapshotView.bounds.width * 1.0) / itemNode.bounds.width, duration: duration, removeOnCompletion: false) } + public func animateOutToReaction(context: AccountContext, theme: PresentationTheme, item: ReactionItem, value: MessageReaction.Reaction, sourceView: UIView, targetView: UIView, hideNode: Bool, forceSwitchToInlineImmediately: Bool = false, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, onHit: (() -> Void)?, completion: @escaping () -> Void) { + let didTriggerExpandedReaction = !"".isEmpty + + let itemNode = ReactionNode(context: context, theme: theme, item: item, icon: .none, animationCache: context.animationCache, animationRenderer: context.animationRenderer, loopIdle: false, isLocked: false, useDirectRendering: true) + if let contents = sourceView.layer.contents { + itemNode.setCustomContents(contents: contents) + } + self.addSubnode(itemNode) + itemNode.frame = sourceView.convert(sourceView.bounds, to: self.view) + itemNode.updateLayout(size: itemNode.frame.size, isExpanded: false, largeExpanded: false, isPreviewing: false, transition: .immediate) + sourceView.layer.isHidden = true + + let switchToInlineImmediately: Bool + if itemNode.item.listAnimation.isVideoEmoji || itemNode.item.listAnimation.isVideoSticker || itemNode.item.listAnimation.isAnimatedSticker || itemNode.item.listAnimation.isStaticEmoji { + switch itemNode.item.reaction.rawValue { + case .builtin: + switchToInlineImmediately = forceSwitchToInlineImmediately + case .custom: + switchToInlineImmediately = !didTriggerExpandedReaction + case .stars: + switchToInlineImmediately = forceSwitchToInlineImmediately + } + } else { + switchToInlineImmediately = !didTriggerExpandedReaction + } + + if hideNode { + if let animateTargetContainer = animateTargetContainer { + animateTargetContainer.isHidden = true + targetView.isHidden = true + } else { + targetView.alpha = 0.0 + targetView.layer.animateAlpha(from: targetView.alpha, to: 0.0, duration: 0.2) + } + } + + itemNode.isExtracted = true + let selfSourceRect = itemNode.view.convert(itemNode.view.bounds, to: self.view) + + var selfTargetBounds = targetView.bounds + if case .builtin = itemNode.item.reaction.rawValue { + selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5) + } else if case .stars = itemNode.item.reaction.rawValue { + selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5) + } + + let selfTargetRect = self.view.convert(selfTargetBounds, from: targetView) + + var expandedSize: CGSize = selfTargetRect.size + if didTriggerExpandedReaction { + if itemNode.item.listAnimation.isVideoEmoji || itemNode.item.listAnimation.isVideoSticker || itemNode.item.listAnimation.isStaticEmoji { + expandedSize = CGSize(width: 80.0, height: 80.0) + } else { + 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) + + var effectFrame: CGRect + let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0 + if didTriggerExpandedReaction { + let expandFactor: CGFloat = 0.5 + effectFrame = expandedFrame.insetBy(dx: -expandedFrame.width * expandFactor, dy: -expandedFrame.height * expandFactor).offsetBy(dx: incomingMessage ? (expandedFrame.width - 50.0) : (-expandedFrame.width + 50.0), dy: 0.0) + } else { + effectFrame = expandedFrame.insetBy(dx: -expandedSize.width, dy: -expandedSize.height) + if itemNode.item.isCustom { + effectFrame = effectFrame.insetBy(dx: -expandedSize.width, dy: -expandedSize.height) + } + } + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear) + + self.addSubnode(itemNode) + itemNode.position = expandedFrame.center + transition.updateBounds(node: itemNode, bounds: CGRect(origin: CGPoint(), size: expandedFrame.size)) + itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, largeExpanded: didTriggerExpandedReaction, isPreviewing: false, transition: transition) + + let additionalAnimationNode: DefaultAnimatedStickerNodeImpl? + var genericAnimationView: AnimationView? + + var additionalAnimation: TelegramMediaFile? + if didTriggerExpandedReaction { + additionalAnimation = itemNode.item.largeApplicationAnimation + } else { + additionalAnimation = itemNode.item.applicationAnimation + } + + if let additionalAnimation = additionalAnimation { + let additionalAnimationNodeValue = DefaultAnimatedStickerNodeImpl() + additionalAnimationNode = additionalAnimationNodeValue + if didTriggerExpandedReaction { + if incomingMessage { + additionalAnimationNodeValue.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + } + } + + additionalAnimationNodeValue.setup(source: AnimatedStickerResourceSource(account: itemNode.context.account, resource: additionalAnimation.resource), width: Int(effectFrame.width * 2.0), height: Int(effectFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(additionalAnimation.resource.id))) + additionalAnimationNodeValue.frame = effectFrame + additionalAnimationNodeValue.updateLayout(size: effectFrame.size) + self.addSubnode(additionalAnimationNodeValue) + } else if itemNode.item.isCustom { + additionalAnimationNode = nil + + var effectData: Data? + if didTriggerExpandedReaction { + if let url = getAppBundle().url(forResource: "generic_reaction_effect", withExtension: "json") { + effectData = try? Data(contentsOf: url) + } + } else if let genericReactionEffect = self.genericReactionEffect, let data = try? Data(contentsOf: URL(fileURLWithPath: genericReactionEffect)) { + effectData = TGGUnzipData(data, 5 * 1024 * 1024) ?? data + } else { + if let url = getAppBundle().url(forResource: "generic_reaction_small_effect", withExtension: "json") { + effectData = try? Data(contentsOf: url) + } + } + + if let effectData = effectData, let composition = try? Animation.from(data: effectData) { + let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable)) + view.animationSpeed = 1.0 + view.backgroundColor = nil + view.isOpaque = false + + if incomingMessage { + view.layer.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + } + + genericAnimationView = view + + let animationCache = itemNode.context.animationCache + let animationRenderer = itemNode.context.animationRenderer + + for i in 1 ... 32 { + let allLayers = view.allLayers(forKeypath: AnimationKeypath(keypath: "placeholder_\(i)")) + for animationLayer in allLayers { + let baseItemLayer = InlineStickerItemLayer( + context: itemNode.context, + userLocation: .other, + attemptSynchronousLoad: false, + emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: itemNode.item.listAnimation.fileId.id, file: itemNode.item.listAnimation), + file: itemNode.item.listAnimation, + cache: animationCache, + renderer: animationRenderer, + placeholderColor: UIColor(white: 0.0, alpha: 0.0), + pointSize: CGSize(width: didTriggerExpandedReaction ? 64.0 : 32.0, height: didTriggerExpandedReaction ? 64.0 : 32.0) + ) + + if let sublayers = animationLayer.sublayers { + for sublayer in sublayers { + sublayer.isHidden = true + } + } + + baseItemLayer.isVisibleForAnimations = true + baseItemLayer.frame = CGRect(origin: CGPoint(x: -0.0, y: -0.0), size: CGSize(width: 500.0, height: 500.0)) + animationLayer.addSublayer(baseItemLayer) + } + } + + if didTriggerExpandedReaction { + view.frame = effectFrame.insetBy(dx: -10.0, dy: -10.0).offsetBy(dx: incomingMessage ? 22.0 : -22.0, dy: 0.0) + } else { + view.frame = effectFrame.insetBy(dx: -20.0, dy: -20.0) + } + self.view.addSubview(view) + } + } else { + additionalAnimationNode = nil + } + + var mainAnimationCompleted = false + var additionalAnimationCompleted = false + let intermediateCompletion: () -> Void = { + if mainAnimationCompleted && additionalAnimationCompleted { + completion() + } + } + + if let additionalAnimationNode = additionalAnimationNode { + additionalAnimationNode.completed = { _ in + additionalAnimationCompleted = true + intermediateCompletion() + } + } else if let genericAnimationView = genericAnimationView { + genericAnimationView.play(completion: { _ in + additionalAnimationCompleted = true + intermediateCompletion() + }) + } else { + additionalAnimationCompleted = true + } + + transition.animatePositionWithKeyframes(node: itemNode, keyframes: generateParabollicMotionKeyframes(from: selfSourceRect.center, to: expandedFrame.center, elevation: 30.0), completion: { [weak itemNode, weak targetView, weak animateTargetContainer] _ in + let afterCompletion: () -> Void = { + if didTriggerExpandedReaction { + return + } + guard let itemNode = itemNode else { + return + } + if let animateTargetContainer = animateTargetContainer { + animateTargetContainer.isHidden = false + } + + if let targetView = targetView { + targetView.isHidden = false + targetView.alpha = 1.0 + targetView.layer.removeAnimation(forKey: "opacity") + } + + HapticFeedback().tap() + onHit?() + + if let targetView = targetView as? ReactionIconView { + if switchToInlineImmediately { + targetView.updateIsAnimationHidden(isAnimationHidden: false, transition: .immediate) + itemNode.isHidden = true + } else { + targetView.updateIsAnimationHidden(isAnimationHidden: true, transition: .immediate) + targetView.addSubnode(itemNode) + itemNode.frame = selfTargetBounds + } + } else if let targetView = targetView as? UIImageView { + itemNode.isHidden = true + targetView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) + targetView.layer.animateScale(from: 0.2, to: 1.0, duration: 0.12) + } + + if switchToInlineImmediately { + mainAnimationCompleted = true + intermediateCompletion() + } + } + + if switchToInlineImmediately { + afterCompletion() + } else { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: afterCompletion) + } + }) + + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.15 * UIView.animationDurationFactor(), execute: { + additionalAnimationNode?.visibility = true + if let animateTargetContainer = animateTargetContainer { + animateTargetContainer.isHidden = false + animateTargetContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + animateTargetContainer.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + } + }) + + if !switchToInlineImmediately { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + min(5.0, 2.0 * UIView.animationDurationFactor()), execute: { + if didTriggerExpandedReaction { + self.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: hideNode, completion: { [weak self] in + if let strongSelf = self, didTriggerExpandedReaction, let addStandaloneReactionAnimation = addStandaloneReactionAnimation { + let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: strongSelf.genericReactionEffect) + + addStandaloneReactionAnimation(standaloneReactionAnimation) + + standaloneReactionAnimation.animateReactionSelection( + context: context, + theme: theme, + animationCache: context.animationCache, + reaction: itemNode.item, + avatarPeers: [], + playHaptic: false, + isLarge: false, + targetView: targetView, + addStandaloneReactionAnimation: nil, + completion: { [weak standaloneReactionAnimation] in + if let _ = standaloneReactionAnimation?.supernode { + standaloneReactionAnimation?.removeFromSupernode() + } else { + standaloneReactionAnimation?.view.removeFromSuperview() + } + } + ) + } + + mainAnimationCompleted = true + intermediateCompletion() + }) + } else { + if hideNode { + targetView.alpha = 1.0 + targetView.isHidden = false + if let targetView = targetView as? ReactionIconView { + targetView.updateIsAnimationHidden(isAnimationHidden: false, transition: .immediate) + if let _ = itemNode.supernode { + itemNode.removeFromSupernode() + } else { + itemNode.view.removeFromSuperview() + } + } + } + mainAnimationCompleted = true + intermediateCompletion() + } + }) + } + } + public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { self.bounds = self.bounds.offsetBy(dx: 0.0, dy: offset.y) transition.animateOffsetAdditive(node: self, offset: -offset.y) diff --git a/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift index b82d10da0a..f43c61c015 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift @@ -6,9 +6,6 @@ extension ReactionsMessageAttribute { func withUpdatedResults(_ reactions: Api.MessageReactions) -> ReactionsMessageAttribute { switch reactions { case let .messageReactions(flags, results, recentReactions, topReactors): - //TODO:release - let _ = topReactors - let min = (flags & (1 << 0)) != 0 let canViewList = (flags & (1 << 2)) != 0 let isTags = (flags & (1 << 3)) != 0 @@ -57,7 +54,23 @@ extension ReactionsMessageAttribute { } } } - return ReactionsMessageAttribute(canViewList: canViewList, isTags: isTags, reactions: reactions, recentPeers: parsedRecentReactions) + + var topPeers: [ReactionsMessageAttribute.TopPeer] = [] + if let topReactors { + for item in topReactors { + switch item { + case let .messageReactor(flags, peerId, count): + topPeers.append(ReactionsMessageAttribute.TopPeer( + peerId: peerId.peerId, + count: count, + isTop: (flags & (1 << 0)) != 0, + isMy: (flags & (1 << 1)) != 0) + ) + } + } + } + + return ReactionsMessageAttribute(canViewList: canViewList, isTags: isTags, reactions: reactions, recentPeers: parsedRecentReactions, topPeers: topPeers) } } } @@ -179,7 +192,7 @@ public func mergedMessageReactions(attributes: [MessageAttribute], isTags: Bool) recentPeers = updatedRecentPeers if !reactions.isEmpty { - result = ReactionsMessageAttribute(canViewList: current?.canViewList ?? false, isTags: current?.isTags ?? isTags, reactions: reactions, recentPeers: recentPeers) + result = ReactionsMessageAttribute(canViewList: current?.canViewList ?? false, isTags: current?.isTags ?? isTags, reactions: reactions, recentPeers: recentPeers, topPeers: current?.topPeers ?? []) } else { result = nil } @@ -198,9 +211,9 @@ public func mergedMessageReactions(attributes: [MessageAttribute], isTags: Bool) reactions.remove(at: index) } reactions.insert(MessageReaction(value: .stars, count: updatedCount, chosenOrder: -1), at: 0) - return ReactionsMessageAttribute(canViewList: current?.canViewList ?? false, isTags: current?.isTags ?? isTags, reactions: reactions, recentPeers: result.recentPeers) + return ReactionsMessageAttribute(canViewList: current?.canViewList ?? false, isTags: current?.isTags ?? isTags, reactions: reactions, recentPeers: result.recentPeers, topPeers: result.topPeers) } else { - return ReactionsMessageAttribute(canViewList: current?.canViewList ?? false, isTags: current?.isTags ?? isTags, reactions: [MessageReaction(value: .stars, count: pendingStars.count, chosenOrder: -1)], recentPeers: []) + return ReactionsMessageAttribute(canViewList: current?.canViewList ?? false, isTags: current?.isTags ?? isTags, reactions: [MessageReaction(value: .stars, count: pendingStars.count, chosenOrder: -1)], recentPeers: [], topPeers: []) } } else { return result @@ -211,8 +224,6 @@ extension ReactionsMessageAttribute { convenience init(apiReactions: Api.MessageReactions) { switch apiReactions { case let .messageReactions(flags, results, recentReactions, topReactors): - //TODO:release - let _ = topReactors let canViewList = (flags & (1 << 2)) != 0 let isTags = (flags & (1 << 3)) != 0 let parsedRecentReactions: [ReactionsMessageAttribute.RecentPeer] @@ -234,6 +245,21 @@ extension ReactionsMessageAttribute { parsedRecentReactions = [] } + var topPeers: [ReactionsMessageAttribute.TopPeer] = [] + if let topReactors { + for item in topReactors { + switch item { + case let .messageReactor(flags, peerId, count): + topPeers.append(ReactionsMessageAttribute.TopPeer( + peerId: peerId.peerId, + count: count, + isTop: (flags & (1 << 0)) != 0, + isMy: (flags & (1 << 1)) != 0) + ) + } + } + } + self.init( canViewList: canViewList, isTags: isTags, @@ -247,7 +273,8 @@ extension ReactionsMessageAttribute { } } }, - recentPeers: parsedRecentReactions + recentPeers: parsedRecentReactions, + topPeers: topPeers ) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift index 7a8bc51b02..ca2e16c90c 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift @@ -328,10 +328,39 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { } } + public struct TopPeer: Equatable, PostboxCoding { + public var peerId: PeerId + public var count: Int32 + public var isTop: Bool + public var isMy: Bool + + public init(peerId: PeerId, count: Int32, isTop: Bool, isMy: Bool) { + self.peerId = peerId + self.count = count + self.isMy = isMy + self.isTop = isTop + } + + public init(decoder: PostboxDecoder) { + self.peerId = PeerId(decoder.decodeInt64ForKey("p", orElse: 0)) + self.count = decoder.decodeInt32ForKey("c", orElse: 0) + self.isTop = decoder.decodeBoolForKey("t", orElse: false) + self.isMy = decoder.decodeBoolForKey("m", orElse: false) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt64(self.peerId.toInt64(), forKey: "p") + encoder.encodeInt32(self.count, forKey: "c") + encoder.encodeBool(self.isTop, forKey: "t") + encoder.encodeBool(self.isMy, forKey: "m") + } + } + public let canViewList: Bool public let isTags: Bool public let reactions: [MessageReaction] public let recentPeers: [RecentPeer] + public let topPeers: [TopPeer] public var associatedPeerIds: [PeerId] { return self.recentPeers.map(\.peerId) @@ -357,11 +386,12 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { return result } - public init(canViewList: Bool, isTags: Bool, reactions: [MessageReaction], recentPeers: [RecentPeer]) { + public init(canViewList: Bool, isTags: Bool, reactions: [MessageReaction], recentPeers: [RecentPeer], topPeers: [TopPeer]) { self.canViewList = canViewList self.isTags = isTags self.reactions = reactions self.recentPeers = recentPeers + self.topPeers = topPeers } required public init(decoder: PostboxDecoder) { @@ -369,6 +399,7 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { self.isTags = decoder.decodeBoolForKey("tg", orElse: false) self.reactions = decoder.decodeObjectArrayWithDecoderForKey("r") self.recentPeers = decoder.decodeObjectArrayWithDecoderForKey("rp") + self.topPeers = decoder.decodeObjectArrayWithDecoderForKey("tp") } public func encode(_ encoder: PostboxEncoder) { @@ -376,6 +407,7 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { encoder.encodeBool(self.isTags, forKey: "tg") encoder.encodeObjectArray(self.reactions, forKey: "r") encoder.encodeObjectArray(self.recentPeers, forKey: "rp") + encoder.encodeObjectArray(self.topPeers, forKey: "tp") } public static func ==(lhs: ReactionsMessageAttribute, rhs: ReactionsMessageAttribute) -> Bool { @@ -391,6 +423,9 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { if lhs.recentPeers != rhs.recentPeers { return false } + if lhs.topPeers != rhs.topPeers { + return false + } return true } @@ -412,7 +447,8 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { var recentPeer = recentPeer recentPeer.isUnseen = false return recentPeer - } + }, + topPeers: self.topPeers ) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 00028d4736..4434370ed2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1275,9 +1275,9 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let reactions: ReactionsMessageAttribute if shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) { - reactions = ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: []) + reactions = ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: [], topPeers: []) } else { - reactions = mergedMessageReactions(attributes: item.message.attributes, isTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId)) ?? ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: []) + reactions = mergedMessageReactions(attributes: item.message.attributes, isTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId)) ?? ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: [], topPeers: []) } var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))? if !reactions.reactions.isEmpty { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index afb7056b82..3945bc6377 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -2039,9 +2039,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let bubbleReactions: ReactionsMessageAttribute if needReactions { - bubbleReactions = mergedMessageReactions(attributes: item.message.attributes, isTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId)) ?? ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: []) + bubbleReactions = mergedMessageReactions(attributes: item.message.attributes, isTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId)) ?? ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: [], topPeers: []) } else { - bubbleReactions = ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: []) + bubbleReactions = ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: [], topPeers: []) } if !bubbleReactions.reactions.isEmpty && !item.presentationData.isPreview { bottomNodeMergeStatus = .Both diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index f9c0c49728..efc4d58fcd 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -595,9 +595,9 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco let reactions: ReactionsMessageAttribute if shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) { - reactions = ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: []) + reactions = ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: [], topPeers: []) } else { - reactions = mergedMessageReactions(attributes: item.message.attributes, isTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId)) ?? ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: []) + reactions = mergedMessageReactions(attributes: item.message.attributes, isTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId)) ?? ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: [], topPeers: []) } var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))? diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift index bb3a4477b4..430dbd7bf9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift @@ -539,7 +539,7 @@ public final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleConte } return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in - let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes, isTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId)) ?? ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: []) + let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes, isTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId)) ?? ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: [], topPeers: []) let buttonsUpdate = buttonsNode.prepareUpdate( context: item.context, presentationData: item.presentationData, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index ae7e32a202..df2ae43e32 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -839,9 +839,9 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { let reactions: ReactionsMessageAttribute if shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) { - reactions = ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: []) + reactions = ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: [], topPeers: []) } else { - reactions = mergedMessageReactions(attributes: item.message.attributes, isTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId)) ?? ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: []) + reactions = mergedMessageReactions(attributes: item.message.attributes, isTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId)) ?? ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: [], topPeers: []) } var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))? diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index 23e6c617c8..8b1be4cc5a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -174,7 +174,7 @@ private final class BadgeComponent: Component { private let badgeShapeLayer = SimpleShapeLayer() private let badgeForeground: SimpleLayer - private let badgeIcon: UIImageView + let badgeIcon: UIImageView private let badgeLabel: BadgeLabelView private let badgeLabelMaskView = UIImageView() @@ -480,17 +480,20 @@ private final class PeerComponent: Component { let theme: PresentationTheme let strings: PresentationStrings let peer: EnginePeer + let count: Int init( context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, - peer: EnginePeer + peer: EnginePeer, + count: Int ) { self.context = context self.theme = theme self.strings = strings self.peer = peer + self.count = count } static func ==(lhs: PeerComponent, rhs: PeerComponent) -> Bool { @@ -506,6 +509,9 @@ private final class PeerComponent: Component { if lhs.peer != rhs.peer { return false } + if lhs.count != rhs.count { + return false + } return true } @@ -546,7 +552,7 @@ private final class PeerComponent: Component { transition: .immediate, component: AnyComponent(PeerBadgeComponent( theme: component.theme, - title: "800" + title: "\(component.count)" )), environment: {}, containerSize: CGSize(width: 200.0, height: 200.0) @@ -590,21 +596,163 @@ private final class PeerComponent: Component { } } +private final class SliderBackgroundComponent: Component { + let theme: PresentationTheme + let value: CGFloat + let topCutoff: CGFloat? + + init( + theme: PresentationTheme, + value: CGFloat, + topCutoff: CGFloat? + ) { + self.theme = theme + self.value = value + self.topCutoff = topCutoff + } + + static func ==(lhs: SliderBackgroundComponent, rhs: SliderBackgroundComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.value != rhs.value { + return false + } + if lhs.topCutoff != rhs.topCutoff { + return false + } + return true + } + + final class View: UIView { + private let sliderBackground = UIView() + private let sliderForeground = UIView() + private let sliderStars = SliderStarsView() + + private let topForegroundLine = SimpleLayer() + private let topBackgroundLine = SimpleLayer() + private let topForegroundText = ComponentView() + private let topBackgroundText = ComponentView() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.sliderBackground.clipsToBounds = true + + self.sliderForeground.clipsToBounds = true + self.sliderForeground.addSubview(self.sliderStars) + + self.addSubview(self.sliderBackground) + self.addSubview(self.sliderForeground) + + self.sliderBackground.layer.addSublayer(self.topBackgroundLine) + self.sliderForeground.layer.addSublayer(self.topForegroundLine) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: SliderBackgroundComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.sliderBackground.backgroundColor = UIColor(rgb: 0xEEEEEF) + self.sliderForeground.backgroundColor = UIColor(rgb: 0xFFB10D) + self.topForegroundLine.backgroundColor = component.theme.list.plainBackgroundColor.cgColor + self.topBackgroundLine.backgroundColor = UIColor(white: 0.0, alpha: 0.1).cgColor + + transition.setFrame(view: self.sliderBackground, frame: CGRect(origin: CGPoint(), size: availableSize)) + + let sliderMinWidth = availableSize.height + let sliderAreaWidth: CGFloat = availableSize.width - sliderMinWidth + let sliderForegroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: sliderMinWidth + floorToScreenPixels(sliderAreaWidth * component.value), height: availableSize.height)) + transition.setFrame(view: self.sliderForeground, frame: sliderForegroundFrame) + + self.sliderBackground.layer.cornerRadius = availableSize.height * 0.5 + self.sliderForeground.layer.cornerRadius = availableSize.height * 0.5 + + self.sliderStars.frame = CGRect(origin: .zero, size: availableSize) + self.sliderStars.update(size: availableSize, value: component.value) + + self.sliderForeground.isHidden = sliderForegroundFrame.width <= sliderMinWidth + + let topCutoff = component.topCutoff ?? 0.0 + + let topLineFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(sliderAreaWidth * topCutoff), y: 0.0), size: CGSize(width: 1.0, height: availableSize.height)) + transition.setFrame(layer: self.topForegroundLine, frame: topLineFrame) + transition.setFrame(layer: self.topBackgroundLine, frame: topLineFrame) + + //TODO:localize + let topTextSize = self.topForegroundText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "TOP", font: Font.medium(17.0), textColor: UIColor(white: 1.0, alpha: 0.4))) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + let _ = self.topBackgroundText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "TOP", font: Font.medium(17.0), textColor: UIColor(white: 0.0, alpha: 0.1))) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + let topTextFrame = CGRect(origin: CGPoint(x: topLineFrame.maxX + 6.0, y: floor((availableSize.height - topTextSize.height) * 0.5)), size: topTextSize) + if let topForegroundTextView = self.topForegroundText.view, let topBackgroundTextView = self.topBackgroundText.view { + if topForegroundTextView.superview == nil { + topBackgroundTextView.layer.anchorPoint = CGPoint() + self.sliderBackground.addSubview(topBackgroundTextView) + + topForegroundTextView.layer.anchorPoint = CGPoint() + self.sliderForeground.addSubview(topForegroundTextView) + } + + transition.setPosition(view: topForegroundTextView, position: topTextFrame.origin) + transition.setPosition(view: topBackgroundTextView, position: topTextFrame.origin) + + 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 + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + private final class ChatSendStarsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let peer: EnginePeer let balance: Int64? - let topPeers: [EnginePeer] - let completion: (Int64) -> Void + let topPeers: [ChatSendStarsScreen.TopPeer] + let completion: (Int64, ChatSendStarsScreen.TransitionOut) -> Void init( context: AccountContext, peer: EnginePeer, balance: Int64?, - topPeers: [EnginePeer], - completion: @escaping (Int64) -> Void + topPeers: [ChatSendStarsScreen.TopPeer], + completion: @escaping (Int64, ChatSendStarsScreen.TransitionOut) -> Void ) { self.context = context self.peer = peer @@ -664,10 +812,8 @@ private final class ChatSendStarsScreenComponent: Component { private let descriptionText = ComponentView() private let badgeStars = BadgeStarsView() + private let sliderBackground = ComponentView() private let slider = ComponentView() - private let sliderBackground = UIView() - private let sliderForeground = UIView() - private let sliderStars = SliderStarsView() private let badge = ComponentView() private var topPeersLeftSeparator: SimpleLayer? @@ -695,6 +841,8 @@ private final class ChatSendStarsScreenComponent: Component { private var cachedStarImage: (UIImage, PresentationTheme)? private var cachedCloseImage: UIImage? + private var isPastTopCutoff: Bool? + override init(frame: CGRect) { self.bottomOverscrollLimit = 200.0 @@ -740,9 +888,6 @@ private final class ChatSendStarsScreenComponent: Component { self.scrollView.addSubview(self.scrollContentView) - self.sliderForeground.clipsToBounds = true - self.sliderForeground.addSubview(self.sliderStars) - self.addSubview(self.navigationBarContainer) self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) @@ -955,34 +1100,44 @@ private final class ChatSendStarsScreenComponent: Component { containerSize: CGSize(width: availableSize.width - sliderInset * 2.0, height: 30.0) ) let sliderFrame = CGRect(origin: CGPoint(x: sliderInset, y: contentHeight + 127.0), size: sliderSize) - if let sliderView = self.slider.view { + 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) + + var topCutoffFraction: CGFloat? + if let maxCount = component.topPeers.max(by: { $0.count < $1.count })?.count { + let topCutoffFractionValue = CGFloat(maxCount) / CGFloat(1000 - 1) + topCutoffFraction = topCutoffFractionValue + + let isPastCutoff = progressFraction >= topCutoffFractionValue + if let isPastTopCutoff = self.isPastTopCutoff, isPastTopCutoff != isPastCutoff { + HapticFeedback().tap() + } + self.isPastTopCutoff = isPastCutoff + } else { + self.isPastTopCutoff = nil + } + + let _ = self.sliderBackground.update( + transition: transition, + component: AnyComponent(SliderBackgroundComponent( + theme: environment.theme, + value: progressFraction, + topCutoff: topCutoffFraction + )), + environment: {}, + containerSize: sliderBackgroundFrame.size + ) + + if let sliderView = self.slider.view, let sliderBackgroundView = self.sliderBackground.view { if sliderView.superview == nil { self.scrollContentView.addSubview(self.badgeStars) - self.scrollContentView.addSubview(self.sliderBackground) - self.scrollContentView.addSubview(self.sliderForeground) + self.scrollContentView.addSubview(sliderBackgroundView) self.scrollContentView.addSubview(sliderView) } transition.setFrame(view: sliderView, frame: sliderFrame) - self.sliderBackground.backgroundColor = UIColor(rgb: 0xEEEEEF) - self.sliderForeground.backgroundColor = UIColor(rgb: 0xFFB10D) - - 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)) - transition.setFrame(view: self.sliderBackground, frame: sliderBackgroundFrame) - - let progressFraction: CGFloat = CGFloat(self.amount) / CGFloat(1000 - 1) - let sliderMinWidth = sliderBackgroundFrame.height - let sliderAreaWidth: CGFloat = sliderBackgroundFrame.width - sliderMinWidth - let sliderForegroundFrame = CGRect(origin: CGPoint(x: sliderBackgroundFrame.minX, y: sliderBackgroundFrame.minY), size: CGSize(width: sliderMinWidth + floorToScreenPixels(sliderAreaWidth * progressFraction), height: sliderBackgroundFrame.height)) - transition.setFrame(view: self.sliderForeground, frame: sliderForegroundFrame) - - self.sliderBackground.layer.cornerRadius = sliderBackgroundFrame.height * 0.5 - self.sliderForeground.layer.cornerRadius = sliderBackgroundFrame.height * 0.5 - - self.sliderStars.frame = CGRect(origin: .zero, size: sliderBackgroundFrame.size) - self.sliderStars.update(size: sliderBackgroundFrame.size, value: progressFraction) - - self.sliderForeground.isHidden = sliderForegroundFrame.width <= sliderMinWidth + transition.setFrame(view: sliderBackgroundView, frame: sliderBackgroundFrame) var effectiveInertiaDirection = self.inertiaDirection if progressFraction <= 0.03 || progressFraction >= 0.97 { @@ -999,6 +1154,11 @@ private final class ChatSendStarsScreenComponent: Component { environment: {}, containerSize: CGSize(width: 200.0, height: 200.0) ) + + let sliderMinWidth = sliderBackgroundFrame.height + let sliderAreaWidth: CGFloat = sliderBackgroundFrame.width - sliderMinWidth + let sliderForegroundFrame = CGRect(origin: sliderBackgroundFrame.origin, size: CGSize(width: sliderMinWidth + floorToScreenPixels(sliderAreaWidth * progressFraction), height: sliderBackgroundFrame.height)) + var badgeFrame = CGRect(origin: CGPoint(x: sliderForegroundFrame.minX + sliderForegroundFrame.width - floorToScreenPixels(sliderMinWidth * 0.5), y: sliderForegroundFrame.minY - 8.0), size: badgeSize) if let badgeView = self.badge.view as? BadgeComponent.View { if badgeView.superview == nil { @@ -1215,14 +1375,14 @@ private final class ChatSendStarsScreenComponent: Component { var validIds: [EnginePeer.Id] = [] var items: [(itemView: ComponentView, size: CGSize)] = [] for topPeer in component.topPeers { - validIds.append(topPeer.id) + validIds.append(topPeer.peer.id) let itemView: ComponentView - if let current = self.topPeerItems[topPeer.id] { + if let current = self.topPeerItems[topPeer.peer.id] { itemView = current } else { itemView = ComponentView() - self.topPeerItems[topPeer.id] = itemView + self.topPeerItems[topPeer.peer.id] = itemView } let itemSize = itemView.update( @@ -1231,7 +1391,8 @@ private final class ChatSendStarsScreenComponent: Component { context: component.context, theme: environment.theme, strings: environment.strings, - peer: topPeer + peer: topPeer.peer, + count: topPeer.count )), environment: {}, containerSize: CGSize(width: 200.0, height: 200.0) @@ -1306,7 +1467,15 @@ private final class ChatSendStarsScreenComponent: Component { guard let self, let component = self.component else { return } - component.completion(self.amount) + guard let badgeView = self.badge.view as? BadgeComponent.View else { + return + } + component.completion( + self.amount, + ChatSendStarsScreen.TransitionOut( + sourceView: badgeView.badgeIcon + ) + ) self.environment?.controller()?.dismiss() } )), @@ -1399,14 +1568,14 @@ private final class ChatSendStarsScreenComponent: Component { public class ChatSendStarsScreen: ViewControllerComponentContainer { public final class InitialData { - let peer: EnginePeer - let balance: Int64? - let topPeers: [EnginePeer] + fileprivate let peer: EnginePeer + fileprivate let balance: Int64? + fileprivate let topPeers: [ChatSendStarsScreen.TopPeer] fileprivate init( peer: EnginePeer, balance: Int64?, - topPeers: [EnginePeer] + topPeers: [ChatSendStarsScreen.TopPeer] ) { self.peer = peer self.balance = balance @@ -1414,13 +1583,41 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { } } + fileprivate final class TopPeer: Equatable { + let peer: EnginePeer + let count: Int + + init(peer: EnginePeer, count: Int) { + self.peer = peer + self.count = count + } + + static func ==(lhs: TopPeer, rhs: TopPeer) -> Bool { + if lhs.peer != rhs.peer { + return false + } + if lhs.count != rhs.count { + return false + } + return true + } + } + + public final class TransitionOut { + public let sourceView: UIView + + init(sourceView: UIView) { + self.sourceView = sourceView + } + } + private let context: AccountContext private var isDismissed: Bool = false private var presenceDisposable: Disposable? - public init(context: AccountContext, initialData: InitialData, completion: @escaping (Int64) -> Void) { + public init(context: AccountContext, initialData: InitialData, completion: @escaping (Int64, TransitionOut) -> Void) { self.context = context super.init(context: context, component: ChatSendStarsScreenComponent( @@ -1454,7 +1651,7 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { } } - public static func initialData(context: AccountContext, peerId: EnginePeer.Id) -> Signal { + public static func initialData(context: AccountContext, peerId: EnginePeer.Id, topPeers: [ReactionsMessageAttribute.TopPeer]) -> Signal { let balance: Signal if let starsContext = context.starsContext { balance = starsContext.state @@ -1467,19 +1664,33 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { } return combineLatest( - context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)), - context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)), + context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), + EngineDataMap(topPeers.map(\.peerId).map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + ), balance ) - |> map { peer, accountPeer, balance -> InitialData? in - guard let peer, let accountPeer else { + |> map { peerAndTopPeerMap, balance -> InitialData? in + let (peer, topPeerMap) = peerAndTopPeerMap + guard let peer else { return nil } return InitialData( peer: peer, balance: balance, - topPeers: [accountPeer, peer] + topPeers: topPeers.compactMap { topPeer -> ChatSendStarsScreen.TopPeer? in + guard let topPeerValue = topPeerMap[topPeer.peerId] else { + return nil + } + guard let topPeerValue else { + return nil + } + return ChatSendStarsScreen.TopPeer( + peer: topPeerValue, + count: Int(topPeer.count) + ) + } ) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 100fc0b918..98b14e1ed8 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -10922,7 +10922,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } if pane.canReorder() { - items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuReorder, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak pane] _, a in if ignoreNextActions { @@ -10937,7 +10937,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }))) } - items.append(.action(ContextMenuActionItem(text: "Select", icon: { theme in + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuSelect, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in if ignoreNextActions { diff --git a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift index fed06dea72..d8c56614de 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift @@ -314,7 +314,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode { recentPeers.append(ReactionsMessageAttribute.RecentPeer(value: reaction, isLarge: false, isUnseen: false, isMy: true, peerId: accountPeer.id, timestamp: nil)) peers[accountPeer.id] = accountPeer } - attributes.append(ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [MessageReaction(value: reaction, count: 1, chosenOrder: 0)], recentPeers: recentPeers)) + attributes.append(ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [MessageReaction(value: reaction, count: 1, chosenOrder: 0)], recentPeers: recentPeers, topPeers: [])) } let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: chatPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[userPeerId], text: messageText, attributes: attributes, media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: item.availableReactions, accountPeer: item.accountPeer, isCentered: true, isPreview: true, isStandalone: false) diff --git a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift index 701b531c7f..7df2f32548 100644 --- a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift +++ b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift @@ -313,10 +313,16 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { } if self.link == nil { - self.link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in + var previousTimestamp = CACurrentMediaTime() + self.link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in guard let self else { return } + + let timestamp = CACurrentMediaTime() + let deltaTime = max(0.0, min(10.0 / 60.0, timestamp - previousTimestamp)) + previousTimestamp = timestamp + for shockwave in self.shockwaves { shockwave.timeValue += deltaTime * (1.0 / CGFloat(UIView.animationDurationFactor())) } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift index 16ba4e04a4..fc8bd905eb 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift @@ -17,6 +17,8 @@ import SavedTagNameAlertController import PremiumUI import ChatSendStarsScreen import ChatMessageItemCommon +import ChatMessageItemView +import ReactionSelectionNode extension ChatControllerImpl { func presentTagPremiumPaywall() { @@ -161,19 +163,123 @@ extension ChatControllerImpl { self.window?.presentInGlobalOverlay(controller) }) } else { - if case .stars = value { + if case .stars = value, let reactionsAttribute = mergedMessageReactions(attributes: message.attributes, isTags: false) { gesture?.cancel() cancelParentGestures(view: sourceView) - let _ = (ChatSendStarsScreen.initialData(context: self.context, peerId: message.id.peerId) + let _ = (ChatSendStarsScreen.initialData(context: self.context, peerId: message.id.peerId, topPeers: reactionsAttribute.topPeers) |> deliverOnMainQueue).start(next: { [weak self] initialData in guard let self, let initialData else { return } - self.push(ChatSendStarsScreen(context: self.context, initialData: initialData, completion: { [weak self] amount in + self.push(ChatSendStarsScreen(context: self.context, initialData: initialData, completion: { [weak self] amount, transitionOut in guard let self, amount > 0 else { return } + var sourceItemNode: ChatMessageItemView? + self.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if itemNode.item?.message.id == message.id { + sourceItemNode = itemNode + return + } + } + } + + if let itemNode = sourceItemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: .stars) { + var reactionItem: ReactionItem? + + for reaction in availableReactions.reactions { + guard let centerAnimation = reaction.centerAnimation else { + continue + } + guard let aroundAnimation = reaction.aroundAnimation else { + continue + } + if reaction.value == .stars { + reactionItem = ReactionItem( + reaction: ReactionItem.Reaction(rawValue: reaction.value), + appearAnimation: reaction.appearAnimation, + stillAnimation: reaction.selectAnimation, + listAnimation: centerAnimation, + largeListAnimation: reaction.activateAnimation, + applicationAnimation: aroundAnimation, + largeApplicationAnimation: reaction.effectAnimation, + isCustom: false + ) + break + } + } + + if let reactionItem { + let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: self.chatDisplayNode.historyNode.takeGenericReactionEffect()) + + self.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) + + self.chatDisplayNode.addSubnode(standaloneReactionAnimation) + standaloneReactionAnimation.frame = self.chatDisplayNode.bounds + standaloneReactionAnimation.animateOutToReaction( + context: self.context, + theme: self.presentationData.theme, + item: reactionItem, + value: .stars, + sourceView: transitionOut.sourceView, + targetView: targetView, + hideNode: false, + forceSwitchToInlineImmediately: false, + animateTargetContainer: nil, + addStandaloneReactionAnimation: { [weak self] standaloneReactionAnimation in + guard let self else { + return + } + self.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) + standaloneReactionAnimation.frame = self.chatDisplayNode.bounds + self.chatDisplayNode.addSubnode(standaloneReactionAnimation) + }, + onHit: { [weak self, weak itemNode] in + guard let self else { + return + } + if let itemNode, let targetView = itemNode.targetReactionView(value: .stars) { + self.chatDisplayNode.wrappingNode.triggerRipple(at: targetView.convert(targetView.bounds.center, to: self.chatDisplayNode.view)) + } + }, + completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.removeFromSupernode() + } + ) + /*standaloneReactionAnimation.animateReactionSelection( + context: strongSelf.context, + theme: strongSelf.presentationData.theme, + animationCache: strongSelf.controllerInteraction!.presentationContext.animationCache, + reaction: reactionItem, + avatarPeers: [], + playHaptic: false, + isLarge: false, + targetView: targetView, + addStandaloneReactionAnimation: { standaloneReactionAnimation in + guard let strongSelf = self else { + return + } + strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) + standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds + strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) + }, + onHit: { [weak itemNode] in + guard let strongSelf = self else { + return + } + if let itemNode = itemNode, let targetView = itemNode.targetReactionView(value: chosenReaction) { + strongSelf.chatDisplayNode.wrappingNode.triggerRipple(at: targetView.convert(targetView.bounds.center, to: strongSelf.chatDisplayNode.view)) + } + }, + completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.removeFromSupernode() + } + )*/ + } + } + let _ = self.context.engine.messages.sendStarsReaction(id: message.id, count: Int(amount)) let _ = (self.context.engine.stickers.resolveInlineStickers(fileIds: [MessageReaction.starsReactionId])