Message preview and effect improvements

This commit is contained in:
Isaac
2024-05-21 00:02:55 +04:00
parent b420532822
commit fe788ed9ea
26 changed files with 568 additions and 276 deletions

View File

@@ -24,6 +24,8 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/Chat/ChatMessageItem",
"//submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode",
"//submodules/TelegramAnimatedStickerNode",
],
visibility = [
"//visibility:public",

View File

@@ -14,6 +14,10 @@ import ChatControllerInteraction
import ChatMessageItemCommon
import TextFormat
import ChatMessageItem
import ChatMessageTransitionNode
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import LottieMetal
public func chatMessageItemLayoutConstants(_ constants: (ChatMessageItemLayoutConstants, ChatMessageItemLayoutConstants), params: ListViewItemLayoutParams, presentationData: ChatPresentationData) -> ChatMessageItemLayoutConstants {
var result: ChatMessageItemLayoutConstants
@@ -653,6 +657,11 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
open var awaitingAppliedReaction: (MessageReaction.Reaction?, () -> Void)?
private var fetchEffectDisposable: Disposable?
public var playedEffectAnimation: Bool = false
public var effectAnimationNodes: [ChatMessageTransitionNode.DecorationItemNode] = []
public required init(rotated: Bool) {
super.init(layerBacked: false, dynamicBounce: true, rotated: rotated)
if rotated {
@@ -664,6 +673,10 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.fetchEffectDisposable?.dispose()
}
override open func reuse() {
super.reuse()
@@ -710,6 +723,28 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
}
}
public func matchesMessage(id: MessageId) -> Bool {
if let item = self.item {
for (message, _) in item.content {
if message.id == id {
return true
}
}
}
return false
}
public func messages() -> [Message] {
guard let item = self.item else {
return []
}
var messages: [Message] = []
for (message, _) in item.content {
messages.append(message)
}
return messages
}
open func transitionNode(id: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
return nil
}
@@ -888,6 +923,146 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
return self.bounds
}
open func playMessageEffect() {
private func playEffectAnimation(effect: AvailableMessageEffects.MessageEffect, force: Bool) {
guard let item = self.item else {
return
}
if self.playedEffectAnimation && !force {
return
}
self.playedEffectAnimation = true
if let effectAnimation = effect.effectAnimation {
self.playEffectAnimation(resource: effectAnimation.resource)
if self.fetchEffectDisposable == nil {
self.fetchEffectDisposable = freeMediaFileResourceInteractiveFetched(account: item.context.account, userLocation: .other, fileReference: .standalone(media: effectAnimation), resource: effectAnimation.resource).startStrict()
}
} else {
let effectSticker = effect.effectSticker
if let effectFile = effectSticker.videoThumbnails.first {
self.playEffectAnimation(resource: effectFile.resource)
if self.fetchEffectDisposable == nil {
self.fetchEffectDisposable = freeMediaFileResourceInteractiveFetched(account: item.context.account, userLocation: .other, fileReference: .standalone(media: effectSticker), resource: effectFile.resource).startStrict()
}
}
}
}
open func messageEffectTargetView() -> UIView? {
return nil
}
private func playEffectAnimation(resource: MediaResource) {
guard let item = self.item else {
return
}
guard let transitionNode = item.controllerInteraction.getMessageTransitionNode() as? ChatMessageTransitionNode else {
return
}
let source = AnimatedStickerResourceSource(account: item.context.account, resource: resource, fitzModifier: nil)
let animationSize = CGSize(width: 380.0, height: 380.0)
let animationNodeFrame: CGRect
guard let messageEffectView = self.messageEffectTargetView() else {
return
}
animationNodeFrame = animationSize.centered(around: messageEffectView.convert(messageEffectView.bounds, to: self.view).center)
if self.effectAnimationNodes.count >= 2 {
return
}
let incomingMessage = item.message.effectivelyIncoming(item.context.account.peerId)
do {
let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(resource.id)
let additionalAnimationNode: AnimatedStickerNode
var effectiveScale: CGFloat = 1.0
#if targetEnvironment(simulator)
additionalAnimationNode = DirectAnimatedStickerNode()
effectiveScale = 1.4
#else
if "".isEmpty {
additionalAnimationNode = DirectAnimatedStickerNode()
effectiveScale = 1.4
} else {
additionalAnimationNode = LottieMetalAnimatedStickerNode()
}
#endif
additionalAnimationNode.updateLayout(size: animationSize)
additionalAnimationNode.setup(source: source, width: Int(animationSize.width * effectiveScale), height: Int(animationSize.height * effectiveScale), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix))
var animationFrame: CGRect
let offsetScale: CGFloat = 0.3
animationFrame = animationNodeFrame.offsetBy(dx: incomingMessage ? animationNodeFrame.width * offsetScale : -animationNodeFrame.width * offsetScale, dy: -10.0)
animationFrame = animationFrame.offsetBy(dx: 0.0, dy: self.insets.top)
additionalAnimationNode.frame = animationFrame
if incomingMessage {
additionalAnimationNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
}
let decorationNode = transitionNode.add(decorationView: additionalAnimationNode.view, itemNode: self)
additionalAnimationNode.completed = { [weak self, weak decorationNode, weak transitionNode] _ in
guard let decorationNode = decorationNode else {
return
}
self?.effectAnimationNodes.removeAll(where: { $0 === decorationNode })
transitionNode?.remove(decorationNode: decorationNode)
}
additionalAnimationNode.isPlayingChanged = { [weak self, weak decorationNode, weak transitionNode] isPlaying in
if !isPlaying {
guard let decorationNode = decorationNode else {
return
}
self?.effectAnimationNodes.removeAll(where: { $0 === decorationNode })
transitionNode?.remove(decorationNode: decorationNode)
}
}
self.effectAnimationNodes.append(decorationNode)
additionalAnimationNode.visibility = true
}
}
public func removeEffectAnimations() {
for decorationNode in self.effectAnimationNodes {
if let additionalAnimationNode = decorationNode.contentView.asyncdisplaykit_node as? AnimatedStickerNode {
additionalAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak additionalAnimationNode] _ in
additionalAnimationNode?.visibility = false
})
}
}
}
public func currentMessageEffect() -> AvailableMessageEffects.MessageEffect? {
guard let item = self.item else {
return nil
}
var messageEffect: AvailableMessageEffects.MessageEffect?
for attribute in item.message.attributes {
if let attribute = attribute as? EffectMessageAttribute {
if let availableMessageEffects = item.associatedData.availableMessageEffects {
for effect in availableMessageEffects.messageEffects {
if effect.id == attribute.id {
messageEffect = effect
break
}
}
}
break
}
}
return messageEffect
}
public func playMessageEffect(force: Bool) {
if let messageEffect = self.currentMessageEffect() {
self.playEffectAnimation(effect: messageEffect, force: force)
}
}
}