Emoji status and reaction improvements

This commit is contained in:
Ali 2022-08-26 13:33:32 +03:00
parent e26c4a6bf0
commit c535d51621
50 changed files with 848 additions and 379 deletions

View File

@ -7221,6 +7221,7 @@ Sorry for the inconvenience.";
"Channel.AdminLog.AllowedReactionsUpdated" = "%1$@ updated the list of allowed reactions to: %2$@";
"Channel.AdminLog.ReactionsDisabled" = "%1$@ disabled reactions";
"Channel.AdminLog.ReactionsEnabled" = "%1$@ enabled all reactions";
"Contacts.ScanQrCode" = "Scan QR Code";
"Contacts.QrCode.MyCode" = "My QR Code";

View File

@ -23,6 +23,8 @@ swift_library(
"//submodules/MeshAnimationCache:MeshAnimationCache",
"//submodules/Utils/RangeSet:RangeSet",
"//submodules/InAppPurchaseManager:InAppPurchaseManager",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
],
visibility = [
"//visibility:public",

View File

@ -12,6 +12,8 @@ import DeviceLocationManager
import TemporaryCachedPeerDataManager
import MeshAnimationCache
import InAppPurchaseManager
import AnimationCache
import MultiAnimationRenderer
public final class TelegramApplicationOpenUrlCompletion {
public let completion: (Bool) -> Void
@ -885,6 +887,9 @@ public protocol AccountContext: AnyObject {
var cachedGroupCallContexts: AccountGroupCallContextCache { get }
var meshAnimationCache: MeshAnimationCache { get }
var animationCache: AnimationCache { get }
var animationRenderer: MultiAnimationRenderer { get }
var animatedEmojiStickers: [String: [StickerPackItem]] { get }
var userLimits: EngineConfiguration.UserLimits { get }

View File

@ -36,6 +36,7 @@ swift_library(
"//submodules/TelegramCore:TelegramCore",
"//submodules/AnimationCompression:AnimationCompression",
"//submodules/Components/MetalImageView:MetalImageView",
"//submodules/WebPBinding:WebPBinding",
],
visibility = [
"//visibility:public",

View File

@ -7,6 +7,7 @@ import CoreMedia
import ManagedFile
import Accelerate
import TelegramCore
import WebPBinding
private let sharedStoreQueue = Queue.concurrentDefaultQueue()
@ -282,6 +283,7 @@ final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
private let width: Int
private let height: Int
private let cache: VideoStickerFrameSourceCache?
private let image: UIImage?
private let bytesPerRow: Int
var frameCount: Int
let frameRate: Int
@ -290,7 +292,11 @@ final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
private let source: SoftwareVideoSource?
var frameIndex: Int {
return self.currentFrame % self.frameCount
if self.frameCount == 0 {
return 0
} else {
return self.currentFrame % self.frameCount
}
}
init?(queue: Queue, path: String, width: Int, height: Int, cachePathPrefix: String?, unpremultiplyAlpha: Bool = true) {
@ -307,11 +313,18 @@ final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
if useCache, let cache = self.cache, cache.frameCount > 0 {
self.source = nil
self.image = nil
self.frameRate = Int(cache.frameRate)
self.frameCount = Int(cache.frameCount)
} else if let data = try? Data(contentsOf: URL(fileURLWithPath: path)), let image = WebP.convert(fromWebP: data) {
self.source = nil
self.image = image
self.frameRate = 1
self.frameCount = 1
} else {
let source = SoftwareVideoSource(path: path, hintVP9: true, unpremultiplyAlpha: unpremultiplyAlpha)
self.source = source
self.image = nil
self.frameRate = min(30, source.getFramerate())
self.frameCount = 0
}
@ -331,7 +344,15 @@ final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
self.currentFrame += 1
if draw {
if useCache, let cache = self.cache, let yuvData = cache.readUncompressedYuvaFrame(index: frameIndex) {
if let image = self.image {
let context = DrawingContext(size: CGSize(width: self.width, height: self.height), scale: 1.0, opaque: false, clear: true, bytesPerRow: self.bytesPerRow)
context.withFlippedContext { c in
c.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: context.size))
}
let frameData = Data(bytes: context.bytes, count: self.bytesPerRow * self.height)
return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount, multiplyAlpha: true)
} else if useCache, let cache = self.cache, let yuvData = cache.readUncompressedYuvaFrame(index: frameIndex) {
return AnimatedStickerFrame(data: yuvData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.width * 2, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount)
} else if let source = self.source {
let frameAndLoop = source.readFrame(maxPts: nil)

View File

@ -336,10 +336,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
self.presentController = presentController
self.makeEntityInputView = makeEntityInputView
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationRenderer = MultiAnimationRendererImpl()
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
var hasSpoilers = true
if presentationInterfaceState.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat {

View File

@ -190,10 +190,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.presentationData = (context.sharedContext.currentPresentationData.with { $0 })
self.presentationDataValue.set(.single(self.presentationData))
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationRenderer = MultiAnimationRendererImpl()
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
self.titleView = ChatListTitleView(
context: context,
@ -327,20 +325,25 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return (data.isLockable, false)
}
let peerStatus: Signal<NetworkStatusTitle.Status?, NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> NetworkStatusTitle.Status? in
guard case let .user(user) = peer else {
return nil
}
if let emojiStatus = user.emojiStatus {
return .emoji(emojiStatus)
} else if user.isPremium {
return .premium
} else {
return nil
let peerStatus: Signal<NetworkStatusTitle.Status?, NoError>
if filter != nil || groupId != .root {
peerStatus = .single(nil)
} else {
peerStatus = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> NetworkStatusTitle.Status? in
guard case let .user(user) = peer else {
return nil
}
if let emojiStatus = user.emojiStatus {
return .emoji(emojiStatus)
} else if user.isPremium {
return .premium
} else {
return nil
}
}
|> distinctUntilChanged
}
|> distinctUntilChanged
let previousEditingAndNetworkStateValue = Atomic<(Bool, AccountNetworkState)?>(value: nil)
if !self.hideNetworkActivityStatus {
@ -858,7 +861,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
areUnicodeEmojiEnabled: false,
areCustomEmojiEnabled: true,
chatPeerId: self.context.account.peerId
)
),
destinationItemView: { [weak sourceView] in
return sourceView
}
), in: .window(.root))
}

View File

@ -384,6 +384,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
containerSize: CGSize(width: 22.0, height: 22.0)
)
titleCredibilityIconTransition.setFrame(view: titleCredibilityIconView, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 2.0, y: floorToScreenPixels(titleFrame.midY - titleIconSize.height / 2.0)), size: titleIconSize))
titleCredibilityIconView.isHidden = self.title.activity
} else {
if let titleCredibilityIconView = self.titleCredibilityIconView {
self.titleCredibilityIconView = nil

View File

@ -597,7 +597,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
self.readStats = readStats
var mergedItems: [EngineMessageReactionListContext.Item] = listState.items
if !listState.canLoadMore, let readStats = readStats {
if !listState.canLoadMore, let readStats = readStats {
var existingPeers = Set(mergedItems.map(\.peer.id))
for peer in readStats.peers {
if !existingPeers.contains(peer.id) {
@ -607,10 +607,6 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
}
}
/*for _ in 0 ..< 5 {
mergedItems.append(contentsOf: mergedItems)
}*/
self.mergedItems = mergedItems
}

View File

@ -974,7 +974,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
strongSelf.statusNode.frame = statusFrame
transition.animatePositionAdditive(node: strongSelf.statusNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0))
if let credibilityIcon = credibilityIcon, let animationCache = item.animationCache, let animationRenderer = item.animationRenderer {
if let credibilityIcon = credibilityIcon {
let animationCache = item.context.animationCache
let animationRenderer = item.context.animationRenderer
let credibilityIconView: ComponentHostView<Empty>
if let current = strongSelf.credibilityIconView {
credibilityIconView = current

View File

@ -576,12 +576,15 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode {
}
self.highlightBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
self.highlightBackgroundNode.frame = CGRect(origin: CGPoint(), size: size)
self.buttonNode.frame = CGRect(origin: CGPoint(), size: size)
return size
}
func setActualSize(size: CGSize, transition: ContainedViewLayoutTransition) {
self.highlightBackgroundNode.frame = CGRect(origin: CGPoint(), size: size)
self.buttonNode.frame = CGRect(origin: CGPoint(), size: size)
}
func updateTheme(presentationData: PresentationData) {
self.backgroundColor = presentationData.theme.contextMenu.backgroundColor
}
@ -802,6 +805,7 @@ final class ContextActionsContainerNode: ASDisplayNode {
contentSize.height += 8.0
let textSelectionTipSize = textSelectionTipNode.updateLayout(widthClass: widthClass, width: actionsSize.width, transition: transition)
transition.updateFrame(node: textSelectionTipNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: textSelectionTipSize))
textSelectionTipNode.setActualSize(size: textSelectionTipSize, transition: transition)
contentSize.height += textSelectionTipSize.height
}

View File

@ -963,7 +963,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
return (size, apparentHeight)
}
func updateTip(presentationData: PresentationData, width: CGFloat, transition: ContainedViewLayoutTransition) -> (node: ASDisplayNode, height: CGFloat)? {
func updateTip(presentationData: PresentationData, width: CGFloat, transition: ContainedViewLayoutTransition) -> (node: InnerTextSelectionTipContainerNode, height: CGFloat)? {
if let tip = self.tip {
var updatedTransition = transition
if let tipNode = self.tipNode, tipNode.tip == tip {
@ -1172,7 +1172,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
let animateAppearingContainers = transition.isAnimated && !self.dismissingItemContainers.isEmpty
struct TipLayout {
var tipNode: ASDisplayNode
var tipNode: InnerTextSelectionTipContainerNode
var tipHeight: CGFloat
}
@ -1320,11 +1320,13 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
self.addSubnode(tip.tipNode)
animateTipIn = transition.isAnimated
tip.tipNode.frame = CGRect(origin: CGPoint(x: previousNavigationContainerFrame.minX, y: previousNavigationContainerFrame.maxY + tipSpacing), size: CGSize(width: itemLayouts[i].size.width, height: tip.tipHeight))
tip.tipNode.setActualSize(size: tip.tipNode.bounds.size, transition: .immediate)
}
let tipAlpha: CGFloat = itemLayouts[i].alphaTransitionFraction
tipTransition.updateFrame(node: tip.tipNode, frame: CGRect(origin: CGPoint(x: navigationContainerFrame.minX, y: navigationContainerFrame.maxY + tipSpacing), size: CGSize(width: itemLayouts[i].size.width, height: tip.tipHeight)), beginWithCurrentState: true)
tip.tipNode.setActualSize(size: tip.tipNode.bounds.size, transition: tipTransition)
if animateTipIn {
tip.tipNode.alpha = tipAlpha

View File

@ -671,7 +671,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
reactionContextNodeTransition = .immediate
}
reactionContextNodeTransition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true)
reactionContextNode.updateLayout(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), anchorRect: contentRect.offsetBy(dx: contentParentGlobalFrame.minX, dy: 0.0), isAnimatingOut: isAnimatingOut, transition: reactionContextNodeTransition)
reactionContextNode.updateLayout(size: layout.size, insets: UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: 0.0, right: layout.safeInsets.right), anchorRect: contentRect.offsetBy(dx: contentParentGlobalFrame.minX, dy: 0.0), isAnimatingOut: isAnimatingOut, transition: reactionContextNodeTransition)
self.proposedReactionsPositionLock = contentRect.minY - 18.0 - reactionContextNode.contentHeight - 46.0
} else {

View File

@ -356,10 +356,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.statusNode = RadialStatusNode(backgroundNodeColor: .clear)
self.statusNode.isUserInteractionEnabled = false
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationRenderer = MultiAnimationRendererImpl()
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
super.init()

View File

@ -36,10 +36,8 @@ public final class HashtagSearchController: TelegramBaseController {
self.peer = peer
self.query = query
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationRenderer = MultiAnimationRendererImpl()
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }

View File

@ -23,6 +23,8 @@ swift_library(
"//submodules/ContextUI:ContextUI",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/AccountContext:AccountContext",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent",
],
visibility = [
"//visibility:public",

View File

@ -13,6 +13,8 @@ import TelegramStringFormatting
import PeerPresenceStatusManager
import ContextUI
import AccountContext
import ComponentFlow
import EmojiStatusComponent
private final class ShimmerEffectNode: ASDisplayNode {
private var currentBackgroundColor: UIColor?
@ -460,7 +462,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
private let labelBadgeNode: ASImageNode
private var labelArrowNode: ASImageNode?
private let statusNode: TextNode
private var credibilityIconNode: ASImageNode?
private var credibilityIconView: ComponentHostView<Empty>?
private var switchNode: SwitchNode?
private var checkNode: ASImageNode?
@ -601,27 +603,34 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
let labelDisclosureFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
var updatedLabelBadgeImage: UIImage?
var currentCredibilityIconImage: UIImage?
var credibilityIcon: EmojiStatusComponent.Content?
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 })
if case .threatSelfAsSaved = item.aliasHandling, item.peer.id == item.context.account.peerId {
} else {
if item.peer.isScam {
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular)
credibilityIcon = .scam(color: item.presentationData.theme.chat.message.incoming.scamColor)
} else if item.peer.isFake {
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular)
credibilityIcon = .fake(color: item.presentationData.theme.chat.message.incoming.scamColor)
} else if case let .user(user) = item.peer, let emojiStatus = user.emojiStatus {
credibilityIcon = .emojiStatus(status: emojiStatus, size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor)
} else if item.peer.isVerified {
currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme)
credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
} else if item.peer.isPremium && !premiumConfiguration.isPremiumDisabled {
currentCredibilityIconImage = PresentationResourcesChatList.premiumIcon(item.presentationData.theme)
credibilityIcon = .premium(color: item.presentationData.theme.list.itemAccentColor)
}
}
var titleIconsWidth: CGFloat = 0.0
if let currentCredibilityIconImage = currentCredibilityIconImage {
titleIconsWidth += 4.0 + currentCredibilityIconImage.size.width
if let credibilityIcon = credibilityIcon {
titleIconsWidth += 4.0
switch credibilityIcon {
case .scam, .fake:
titleIconsWidth += 30.0
default:
titleIconsWidth += 16.0
}
}
var badgeColor: UIColor?
@ -1063,23 +1072,38 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame)
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size))
if let currentCredibilityIconImage = currentCredibilityIconImage {
let iconNode: ASImageNode
if let current = strongSelf.credibilityIconNode {
iconNode = current
if let credibilityIcon = credibilityIcon {
let animationCache = item.context.animationCache
let animationRenderer = item.context.animationRenderer
let credibilityIconView: ComponentHostView<Empty>
if let current = strongSelf.credibilityIconView {
credibilityIconView = current
} else {
iconNode = ASImageNode()
iconNode.isLayerBacked = true
iconNode.displaysAsynchronously = false
iconNode.displayWithoutProcessing = true
strongSelf.containerNode.addSubnode(iconNode)
strongSelf.credibilityIconNode = iconNode
credibilityIconView = ComponentHostView<Empty>()
strongSelf.containerNode.view.addSubview(credibilityIconView)
strongSelf.credibilityIconView = credibilityIconView
}
iconNode.image = currentCredibilityIconImage
transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - currentCredibilityIconImage.size.height / 2.0) - UIScreenPixel), size: currentCredibilityIconImage.size))
} else if let credibilityIconNode = strongSelf.credibilityIconNode {
strongSelf.credibilityIconNode = nil
credibilityIconNode.removeFromSupernode()
let iconSize = credibilityIconView.update(
transition: .immediate,
component: AnyComponent(EmojiStatusComponent(
context: item.context,
animationCache: animationCache,
animationRenderer: animationRenderer,
content: credibilityIcon,
action: nil,
longTapAction: nil,
emojiFileUpdated: nil
)),
environment: {},
containerSize: CGSize(width: 20.0, height: 20.0)
)
transition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize))
} else if let credibilityIconView = strongSelf.credibilityIconView {
strongSelf.credibilityIconView = nil
credibilityIconView.removeFromSuperview()
}
if let currentSwitchNode = currentSwitchNode {
@ -1321,8 +1345,8 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size))
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.statusNode.frame.minY), size: self.statusNode.bounds.size))
if let credibilityIconNode = self.credibilityIconNode {
transition.updateFrame(node: credibilityIconNode, frame: CGRect(origin: CGPoint(x: self.titleNode.frame.maxX + 4.0, y: credibilityIconNode.frame.minY), size: credibilityIconNode.bounds.size))
if let credibilityIconView = self.credibilityIconView {
transition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: self.titleNode.frame.maxX + 4.0, y: credibilityIconView.frame.minY), size: credibilityIconView.bounds.size))
}
var rightLabelInset: CGFloat = 15.0 + params.rightInset

View File

@ -1667,10 +1667,8 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
self.present = present
self.completion = completion
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationRenderer = MultiAnimationRendererImpl()
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
super.init()

View File

@ -127,10 +127,8 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
self.context = context
self.theme = theme
self.animationCache = AnimationCacheImpl(basePath: self.context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationRenderer = MultiAnimationRendererImpl()
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
var reactionMap: [MessageReaction.Reaction: AvailableReactions.Reaction] = [:]
for reaction in reactions {
@ -362,7 +360,7 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
applicationAnimation: aroundAnimation,
largeApplicationAnimation: reaction.effectAnimation,
isCustom: false
), animationCache: self.animationCache, animationRenderer: self.animationRenderer, hasAppearAnimation: false, useDirectRendering: false)
), animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: false, hasAppearAnimation: false, useDirectRendering: false)
containerNode.isUserInteractionEnabled = false
containerNode.addSubnode(itemNode)
self.addSubnode(containerNode)

View File

@ -193,6 +193,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
private var animateInInfo: (centerX: CGFloat, width: CGFloat)?
private var availableReactions: AvailableReactions?
private var availableReactionsDisposable: Disposable?
public init(context: AccountContext, animationCache: AnimationCache, presentationData: PresentationData, items: [ReactionContextItem], getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?, isExpandedUpdated: @escaping (ContainedViewLayoutTransition) -> Void, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void) {
self.context = context
self.presentationData = presentationData
@ -294,10 +297,20 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
self.view.addGestureRecognizer(horizontalExpandRecognizer)
self.horizontalExpandRecognizer = horizontalExpandRecognizer
}
self.availableReactionsDisposable = (context.engine.stickers.availableReactions()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] availableReactions in
guard let strongSelf = self else {
return
}
strongSelf.availableReactions = availableReactions
})
}
deinit {
self.emojiContentDisposable?.dispose()
self.availableReactionsDisposable?.dispose()
}
override public func didLoad() {
@ -371,7 +384,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
contentSize.width = max(46.0, contentSize.width)
contentSize.height = self.currentContentHeight
let sideInset: CGFloat = 11.0
let sideInset: CGFloat = 11.0 + insets.left
let backgroundOffset: CGPoint = CGPoint(x: 22.0, y: -7.0)
var rect: CGRect
@ -459,14 +472,23 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
var topVisibleItems: Int
if self.getEmojiContent != nil {
topVisibleItems = min(self.items.count, visibleItemCount - 1)
if compressionFactor >= 0.6 {
topVisibleItems = min(self.items.count, visibleItemCount)
}
topVisibleItems = min(self.items.count, visibleItemCount)
} else {
topVisibleItems = self.items.count
}
var loopIdle = false
for i in 0 ..< min(self.items.count, visibleItemCount) {
if let reaction = self.items[i].reaction {
switch reaction.rawValue {
case .builtin:
break
case .custom:
loopIdle = true
}
}
}
var validIndices = Set<Int>()
var nextX: CGFloat = sideInset
for i in 0 ..< self.items.count {
@ -519,7 +541,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
if appearBounds.intersects(baseItemFrame) || (self.visibleItemNodes[i] != nil && visibleBounds.intersects(baseItemFrame)) {
validIndices.insert(i)
let itemFrame = baseItemFrame
var itemFrame = baseItemFrame
var isPreviewing = false
if let highlightedReaction = self.highlightedReaction, highlightedReaction == self.items[i].reaction {
@ -540,7 +562,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
itemTransition = .immediate
if case let .reaction(item) = self.items[i] {
itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: item, animationCache: self.animationCache, animationRenderer: self.animationRenderer)
itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: item, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: loopIdle)
maskNode = nil
} else {
itemNode = PremiumReactionsNode(theme: self.presentationData.theme)
@ -570,6 +592,13 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
}
}
if self.getEmojiContent != nil && i == visibleItemCount - 1 {
itemFrame.origin.x -= (1.0 - compressionFactor) * itemFrame.width * 0.5
itemNode.isUserInteractionEnabled = false
} else {
itemNode.isUserInteractionEnabled = true
}
itemTransition.updateFrame(node: itemNode, frame: itemFrame, beginWithCurrentState: true, completion: { [weak self, weak itemNode] completed in
guard let strongSelf = self, let itemNode = itemNode else {
return
@ -588,6 +617,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
if animateIn {
itemNode.appear(animated: !self.context.sharedContext.currentPresentationData.with({ $0 }).reduceMotion)
}
if self.getEmojiContent != nil && i == visibleItemCount - 1 {
transition.updateSublayerTransformScale(node: itemNode, scale: 0.001 * (1.0 - compressionFactor) + 1.0 * compressionFactor)
}
}
}
}
@ -907,25 +940,44 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
emojiContent.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak self] groupId, item, sourceView, sourceRect, sourceLayer, isLongPress in
guard let strongSelf = self, let itemFile = item.itemFile else {
guard let strongSelf = self, let availableReactions = strongSelf.availableReactions, let itemFile = item.itemFile else {
return
}
strongSelf.didTriggerExpandedReaction = isLongPress
var found = false
if let groupId = groupId.base as? String, groupId == "recent" {
for reactionItem in strongSelf.items {
if case let .reaction(reactionItem) = reactionItem {
if reactionItem.stillAnimation.fileId == itemFile.fileId {
found = true
strongSelf.customReactionSource = (sourceView, sourceRect, sourceLayer, reactionItem)
strongSelf.reactionSelected?(reactionItem.updateMessageReaction, isLongPress)
break
}
for reaction in availableReactions.reactions {
guard let centerAnimation = reaction.centerAnimation, let aroundAnimation = reaction.aroundAnimation else {
continue
}
if reaction.selectAnimation.fileId == itemFile.fileId {
found = true
let updateReaction: UpdateMessageReaction
switch reaction.value {
case let .builtin(value):
updateReaction = .builtin(value)
case let .custom(fileId):
updateReaction = .custom(fileId: fileId, file: nil)
}
let 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
)
strongSelf.customReactionSource = (sourceView, sourceRect, sourceLayer, reactionItem)
strongSelf.reactionSelected?(updateReaction, isLongPress)
break
}
}
if !found {
@ -978,7 +1030,26 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
}
})
},
clearGroup: { _ in
clearGroup: { [weak self] groupId in
guard let strongSelf = self else {
return
}
if groupId == AnyHashable("popular") {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize))
var items: [ActionSheetItem] = []
let context = strongSelf.context
items.append(ActionSheetButtonItem(title: presentationData.strings.Emoji_ClearRecent, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let _ = context.engine.stickers.clearRecentlyUsedReactions().start()
}))
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet)
}
},
pushController: { _ in
},
@ -1159,7 +1230,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
}
if let customReactionSource = self.customReactionSource {
let itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: customReactionSource.item, animationCache: self.animationCache, animationRenderer: self.animationRenderer, useDirectRendering: false)
let itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: customReactionSource.item, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: false, useDirectRendering: false)
if let contents = customReactionSource.layer.contents {
itemNode.setCustomContents(contents: contents)
}
@ -1176,7 +1247,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
}
let switchToInlineImmediately: Bool
if itemNode.item.listAnimation.isVideoEmoji || itemNode.item.listAnimation.isVideoSticker || itemNode.item.listAnimation.isAnimatedSticker {
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 = false
@ -1196,11 +1267,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
targetView.isHidden = true
} else {
targetView.alpha = 0.0
targetView.layer.animateAlpha(from: targetView.alpha, to: 0.0, duration: 0.2, completion: { [weak targetView] completed in
if completed {
targetView?.isHidden = true
}
})
targetView.layer.animateAlpha(from: targetView.alpha, to: 0.0, duration: 0.2)
}
}
@ -1216,7 +1283,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
var expandedSize: CGSize = selfTargetRect.size
if self.didTriggerExpandedReaction {
if itemNode.item.listAnimation.isVideoEmoji || itemNode.item.listAnimation.isVideoSticker {
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)
@ -1267,7 +1334,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
} else if itemNode.item.isCustom {
additionalAnimationNode = nil
if let url = getAppBundle().url(forResource: "generic_reaction_effect", withExtension: "json"), let composition = Animation.filepath(url.path) {
if let url = getAppBundle().url(forResource: self.didTriggerExpandedReaction ? "generic_reaction_effect" : "generic_reaction_small_effect", withExtension: "json"), let composition = Animation.filepath(url.path) {
let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
view.animationSpeed = 1.0
view.backgroundColor = nil
@ -1279,36 +1346,40 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
genericAnimationView = view
let animationCache = AnimationCacheImpl(basePath: itemNode.context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
let animationRenderer = MultiAnimationRendererImpl()
let animationCache = itemNode.context.animationCache
let animationRenderer = itemNode.context.animationRenderer
let allLayers = view.allLayers(forKeypath: AnimationKeypath(keypath: "BODY 1 Precomp"))
for animationLayer in allLayers {
let baseItemLayer = InlineStickerItemLayer(
context: itemNode.context,
attemptSynchronousLoad: false,
emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: 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: self.didTriggerExpandedReaction ? 64.0 : 32.0, height: self.didTriggerExpandedReaction ? 64.0 : 32.0)
)
if let sublayers = animationLayer.sublayers {
for sublayer in sublayers {
sublayer.isHidden = true
for i in 1 ... 32 {
let allLayers = view.allLayers(forKeypath: AnimationKeypath(keypath: "placeholder_\(i)"))
for animationLayer in allLayers {
let baseItemLayer = InlineStickerItemLayer(
context: itemNode.context,
attemptSynchronousLoad: false,
emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: 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: self.didTriggerExpandedReaction ? 64.0 : 32.0, height: self.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)
}
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)
}
view.frame = effectFrame.insetBy(dx: -10.0, dy: -10.0).offsetBy(dx: incomingMessage ? 22.0 : -22.0, dy: 0.0)
if self.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 {
@ -1348,14 +1419,19 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
guard let itemNode = itemNode else {
return
}
guard let targetView = targetView as? ReactionIconView else {
return
}
if let animateTargetContainer = animateTargetContainer {
animateTargetContainer.isHidden = false
}
targetView.isHidden = false
targetView.alpha = 1.0
if let targetView = targetView {
targetView.isHidden = false
targetView.alpha = 1.0
targetView.layer.removeAnimation(forKey: "opacity")
}
guard let targetView = targetView as? ReactionIconView else {
return
}
if switchToInlineImmediately {
targetView.updateIsAnimationHidden(isAnimationHidden: false, transition: .immediate)
@ -1619,6 +1695,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
if itemNode.supernode === self.scrollNode && !self.scrollNode.bounds.intersects(itemNode.frame) {
continue
}
if !itemNode.isUserInteractionEnabled {
continue
}
let itemPoint = self.view.convert(point, to: itemNode.view)
if itemNode.bounds.insetBy(dx: -touchInset, dy: -touchInset).contains(itemPoint) {
return itemNode
@ -1704,7 +1783,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
itemNode = currentItemNode
} else {
let animationRenderer = MultiAnimationRendererImpl()
itemNode = ReactionNode(context: context, theme: theme, item: reaction, animationCache: animationCache, animationRenderer: animationRenderer)
itemNode = ReactionNode(context: context, theme: theme, item: reaction, animationCache: animationCache, animationRenderer: animationRenderer, loopIdle: false)
}
self.itemNode = itemNode
@ -1812,7 +1891,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
} else if itemNode.item.isCustom {
additionalAnimationNode = nil
if let url = getAppBundle().url(forResource: "generic_reaction_effect", withExtension: "json"), let composition = Animation.filepath(url.path) {
if let url = getAppBundle().url(forResource: "generic_reaction_small_effect", withExtension: "json"), let composition = Animation.filepath(url.path) {
let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
view.animationSpeed = 1.0
view.backgroundColor = nil
@ -1824,36 +1903,36 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
genericAnimationView = view
let animationCache = AnimationCacheImpl(basePath: itemNode.context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
let animationRenderer = MultiAnimationRendererImpl()
let animationCache = itemNode.context.animationCache
let animationRenderer = itemNode.context.animationRenderer
let allLayers = view.allLayers(forKeypath: AnimationKeypath(keypath: "BODY 1 Precomp"))
for animationLayer in allLayers {
let baseItemLayer = InlineStickerItemLayer(
context: itemNode.context,
attemptSynchronousLoad: false,
emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: 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: 32.0, height: 32.0)
)
for i in 1 ... 7 {
let allLayers = view.allLayers(forKeypath: AnimationKeypath(keypath: "placeholder_\(i)"))
for animationLayer in allLayers {
let baseItemLayer = InlineStickerItemLayer(
context: itemNode.context,
attemptSynchronousLoad: false,
emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: 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: 32.0, height: 32.0)
)
if let sublayers = animationLayer.sublayers {
for sublayer in sublayers {
sublayer.isHidden = true
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)
}
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)
}
view.frame = effectFrame.insetBy(dx: -10.0, dy: -10.0).offsetBy(dx: incomingMessage ? 22.0 : -22.0, dy: 0.0)
view.frame = effectFrame.insetBy(dx: -20.0, dy: -20.0)//.offsetBy(dx: incomingMessage ? 22.0 : -22.0, dy: 0.0)
self.view.addSubview(view)
}
} else {

View File

@ -49,6 +49,7 @@ protocol ReactionItemNode: ASDisplayNode {
public final class ReactionNode: ASDisplayNode, ReactionItemNode {
let context: AccountContext
let item: ReactionItem
private let loopIdle: Bool
private let hasAppearAnimation: Bool
private let useDirectRendering: Bool
@ -79,9 +80,10 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
return self.staticAnimationNode.currentFrameImage
}
public init(context: AccountContext, theme: PresentationTheme, item: ReactionItem, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, hasAppearAnimation: Bool = true, useDirectRendering: Bool = false) {
public init(context: AccountContext, theme: PresentationTheme, item: ReactionItem, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, loopIdle: Bool, hasAppearAnimation: Bool = true, useDirectRendering: Bool = false) {
self.context = context
self.item = item
self.loopIdle = loopIdle
self.hasAppearAnimation = hasAppearAnimation
self.useDirectRendering = useDirectRendering
@ -105,7 +107,9 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
}
if strongSelf.animationNode == nil {
strongSelf.staticAnimationNode.isHidden = false
strongSelf.staticAnimationNode.playLoop()
if strongSelf.loopIdle {
strongSelf.staticAnimationNode.playLoop()
}
}
strongSelf.animateInAnimationNode?.removeFromSupernode()
@ -133,6 +137,13 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
if animated {
if self.item.isCustom {
self.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
if self.animationNode == nil {
self.staticAnimationNode.isHidden = false
if self.loopIdle {
self.staticAnimationNode.playLoop()
}
}
} else {
self.animateInAnimationNode?.visibility = true
}
@ -184,11 +195,11 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
}
if largeExpanded {
let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.item.largeListAnimation.resource, isVideo: self.item.largeListAnimation.isVideoSticker || self.item.largeListAnimation.isVideoEmoji)
let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.item.largeListAnimation.resource, isVideo: self.item.largeListAnimation.isVideoSticker || self.item.largeListAnimation.isVideoEmoji || self.item.largeListAnimation.isStaticSticker || self.item.largeListAnimation.isStaticEmoji)
animationNode.setup(source: source, width: Int(expandedAnimationFrame.width * 2.0), height: Int(expandedAnimationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.largeListAnimation.resource.id)))
} else {
let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.item.listAnimation.resource, isVideo: self.item.listAnimation.isVideoSticker || self.item.listAnimation.isVideoEmoji)
let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.item.listAnimation.resource, isVideo: self.item.listAnimation.isVideoSticker || self.item.listAnimation.isVideoEmoji || self.item.listAnimation.isVideoSticker || self.item.listAnimation.isStaticSticker || self.item.listAnimation.isStaticEmoji)
animationNode.setup(source: source, width: Int(expandedAnimationFrame.width * 2.0), height: Int(expandedAnimationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.listAnimation.resource.id)))
}
animationNode.frame = expandedAnimationFrame
@ -240,7 +251,7 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
transition.animateTransformScale(node: customContentsNode, from: customContentsNode.bounds.width / animationFrame.width)
transition.animatePositionAdditive(node: customContentsNode, offset: CGPoint(x: customContentsNode.frame.midX - animationFrame.midX, y: customContentsNode.frame.midY - animationFrame.midY))
if self.item.listAnimation.isVideoEmoji || self.item.listAnimation.isVideoSticker || self.item.listAnimation.isAnimatedSticker {
if self.item.listAnimation.isVideoEmoji || self.item.listAnimation.isVideoSticker || self.item.listAnimation.isAnimatedSticker || self.item.listAnimation.isStaticSticker || self.item.listAnimation.isStaticEmoji {
customContentsNode.alpha = 0.0
customContentsNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
@ -273,7 +284,7 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
self.stillAnimationNode = stillAnimationNode
self.addSubnode(stillAnimationNode)
stillAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource, isVideo: self.item.stillAnimation.isVideoEmoji || self.item.stillAnimation.isVideoSticker), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .loop, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id)))
stillAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource, isVideo: self.item.stillAnimation.isVideoEmoji || self.item.stillAnimation.isVideoSticker || self.item.stillAnimation.isStaticSticker || self.item.stillAnimation.isStaticEmoji), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: self.loopIdle ? .loop : .still(.start), mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id)))
stillAnimationNode.position = animationFrame.center
stillAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size)
stillAnimationNode.updateLayout(size: animationFrame.size)
@ -288,7 +299,9 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
animateInAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1)
strongSelf.staticAnimationNode.isHidden = false
strongSelf.staticAnimationNode.playLoop()
if strongSelf.loopIdle {
strongSelf.staticAnimationNode.playLoop()
}
}
}
stillAnimationNode.visibility = true
@ -329,9 +342,9 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
self.staticAnimationNode.automaticallyLoadFirstFrame = true
if !self.hasAppearAnimation {
self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.largeListAnimation.resource, isVideo: self.item.largeListAnimation.isVideoEmoji || self.item.largeListAnimation.isVideoSticker), width: Int(expandedAnimationFrame.width * 2.0), height: Int(expandedAnimationFrame.height * 2.0), playbackMode: .still(.start), mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.largeListAnimation.resource.id)))
self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.largeListAnimation.resource, isVideo: self.item.largeListAnimation.isVideoEmoji || self.item.largeListAnimation.isVideoSticker || self.item.largeListAnimation.isStaticSticker || self.item.largeListAnimation.isStaticEmoji), width: Int(expandedAnimationFrame.width * 2.0), height: Int(expandedAnimationFrame.height * 2.0), playbackMode: .still(.start), mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.largeListAnimation.resource.id)))
} else {
self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource, isVideo: self.item.stillAnimation.isVideoEmoji || self.item.stillAnimation.isVideoSticker), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .still(.start), mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id)))
self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource, isVideo: self.item.stillAnimation.isVideoEmoji || self.item.stillAnimation.isVideoSticker || self.item.stillAnimation.isStaticSticker || self.item.stillAnimation.isStaticEmoji), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .still(.start), mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id)))
}
self.staticAnimationNode.position = animationFrame.center
self.staticAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size)
@ -339,7 +352,7 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
self.staticAnimationNode.visibility = true
if let animateInAnimationNode = self.animateInAnimationNode {
animateInAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.appearAnimation.resource, isVideo: self.item.appearAnimation.isVideoEmoji || self.item.appearAnimation.isVideoSticker), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.appearAnimation.resource.id)))
animateInAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.appearAnimation.resource, isVideo: self.item.appearAnimation.isVideoEmoji || self.item.appearAnimation.isVideoSticker || self.item.appearAnimation.isStaticSticker || self.item.appearAnimation.isStaticEmoji), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.appearAnimation.resource.id)))
animateInAnimationNode.position = animationFrame.center
animateInAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size)
animateInAnimationNode.updateLayout(size: animationFrame.size)

View File

@ -169,15 +169,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode {
let standaloneReactionAnimation = StandaloneReactionAnimation()
self.standaloneReactionAnimation = standaloneReactionAnimation
let animationCache: AnimationCache
if let current = self.animationCache {
animationCache = current
} else {
animationCache = AnimationCacheImpl(basePath: item.context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationCache = animationCache
}
let animationCache = item.context.animationCache
supernode.addSubnode(standaloneReactionAnimation)
standaloneReactionAnimation.frame = supernode.bounds

View File

@ -63,10 +63,8 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationThemeSettings = presentationThemeSettings
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationRenderer = MultiAnimationRendererImpl()
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
let calendar = Calendar(identifier: .gregorian)
var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: Date())

View File

@ -233,10 +233,8 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate
self.wallpaper = self.presentationData.chatWallpaper
let bubbleCorners = self.presentationData.chatBubbleCorners
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationRenderer = MultiAnimationRendererImpl()
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
self.ready = ready

View File

@ -88,10 +88,8 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationRenderer = MultiAnimationRendererImpl()
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
let calendar = Calendar(identifier: .gregorian)
var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: Date())

View File

@ -1547,19 +1547,9 @@ public final class StickerPackScreenImpl: ViewController {
self.parentNavigationController = parentNavigationController
self.sendSticker = sendSticker
self.actionPerformed = actionPerformed
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
let animationRenderer: MultiAnimationRenderer
/*if #available(iOS 13.0, *) {
animationRenderer = MultiAnimationMetalRendererImpl()
} else {*/
animationRenderer = MultiAnimationRendererImpl()
//}
self.animationRenderer = animationRenderer
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
super.init(navigationBarPresentationData: nil)

View File

@ -68,8 +68,11 @@ private extension PremiumPromoConfiguration {
case let .premiumPromo(statusText, statusEntities, videoSections, videoFiles, periodOptions, _):
self.status = statusText
self.statusEntities = messageTextEntitiesFromApiEntities(statusEntities)
self.currency = currency
self.monthlyAmount = monthlyAmount
let _ = periodOptions
self.currency = "USD"
self.monthlyAmount = 500
//self.currency = currency
//self.monthlyAmount = monthlyAmount
var videos: [String: TelegramMediaFile] = [:]
for (key, document) in zip(videoSections, videoFiles) {
if let file = telegramMediaFileFromApiDocument(document) {

View File

@ -270,7 +270,7 @@ func managedRecentReactions(postbox: Postbox, network: Network) -> Signal<Void,
return fileId.id
}
}, reverseHashOrder: false, forceFetch: false, fetch: { hash in
return network.request(Api.functions.messages.getRecentReactions(limit: 24, hash: hash))
return network.request(Api.functions.messages.getRecentReactions(limit: 100, hash: hash))
|> retryRequest
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
switch result {
@ -321,7 +321,7 @@ func managedTopReactions(postbox: Postbox, network: Network) -> Signal<Void, NoE
return fileId.id
}
}, reverseHashOrder: false, forceFetch: false, fetch: { hash in
return network.request(Api.functions.messages.getTopReactions(limit: 24, hash: hash))
return network.request(Api.functions.messages.getTopReactions(limit: 32, hash: hash))
|> retryRequest
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
switch result {

View File

@ -18,7 +18,7 @@ public enum UpdateMessageReaction {
}
}
public func updateMessageReactionsInteractively(account: Account, messageId: MessageId, reactions: [UpdateMessageReaction], isLarge: Bool) -> Signal<Never, NoError> {
public func updateMessageReactionsInteractively(account: Account, messageId: MessageId, reactions: [UpdateMessageReaction], isLarge: Bool, storeAsRecentlyUsed: Bool) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Void in
let isPremium = (transaction.getPeer(account.peerId) as? TelegramUser)?.isPremium ?? false
let appConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? .defaultValue
@ -57,25 +57,27 @@ public func updateMessageReactionsInteractively(account: Account, messageId: Mes
}
}
for attribute in currentMessage.attributes {
if let attribute = attribute as? ReactionsMessageAttribute {
for updatedReaction in reactions {
if !attribute.reactions.contains(where: { $0.value == updatedReaction.reaction }) {
let recentReactionItem: RecentReactionItem
switch updatedReaction {
case let .builtin(value):
recentReactionItem = RecentReactionItem(.builtin(value))
case let .custom(fileId, file):
if let file = file ?? (transaction.getMedia(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) as? TelegramMediaFile) {
recentReactionItem = RecentReactionItem(.custom(file))
} else {
continue
if storeAsRecentlyUsed {
for attribute in currentMessage.attributes {
if let attribute = attribute as? ReactionsMessageAttribute {
for updatedReaction in reactions {
if !attribute.reactions.contains(where: { $0.value == updatedReaction.reaction }) {
let recentReactionItem: RecentReactionItem
switch updatedReaction {
case let .builtin(value):
recentReactionItem = RecentReactionItem(.builtin(value))
case let .custom(fileId, file):
if let file = file ?? (transaction.getMedia(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) as? TelegramMediaFile) {
recentReactionItem = RecentReactionItem(.custom(file))
} else {
continue
}
}
if let entry = CodableEntry(recentReactionItem) {
let itemEntry = OrderedItemListEntry(id: recentReactionItem.id.rawValue, contents: entry)
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentReactions, item: itemEntry, removeTailIfCountExceeds: 50)
}
}
if let entry = CodableEntry(recentReactionItem) {
let itemEntry = OrderedItemListEntry(id: recentReactionItem.id.rawValue, contents: entry)
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentReactions, item: itemEntry, removeTailIfCountExceeds: 50)
}
}
}
@ -84,7 +86,7 @@ public func updateMessageReactionsInteractively(account: Account, messageId: Mes
var mappedReactions = mappedReactions
let updatedReactions = mergedMessageReactions(attributes: attributes + [PendingReactionsMessageAttribute(accountPeerId: account.peerId, reactions: mappedReactions, isLarge: isLarge)])?.reactions ?? []
let updatedReactions = mergedMessageReactions(attributes: attributes + [PendingReactionsMessageAttribute(accountPeerId: account.peerId, reactions: mappedReactions, isLarge: isLarge, storeAsRecentlyUsed: storeAsRecentlyUsed)])?.reactions ?? []
let updatedOutgoingReactions = updatedReactions.filter(\.isSelected)
if updatedOutgoingReactions.count > maxCount {
let sortedOutgoingReactions = updatedOutgoingReactions.sorted(by: { $0.chosenOrder! < $1.chosenOrder! })
@ -93,7 +95,7 @@ public func updateMessageReactionsInteractively(account: Account, messageId: Mes
})
}
attributes.append(PendingReactionsMessageAttribute(accountPeerId: account.peerId, reactions: mappedReactions, isLarge: isLarge))
attributes.append(PendingReactionsMessageAttribute(accountPeerId: account.peerId, reactions: mappedReactions, isLarge: isLarge, storeAsRecentlyUsed: storeAsRecentlyUsed))
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
@ -106,7 +108,7 @@ private enum RequestUpdateMessageReactionError {
}
private func requestUpdateMessageReaction(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId) -> Signal<Never, RequestUpdateMessageReactionError> {
return postbox.transaction { transaction -> (Peer, [MessageReaction.Reaction]?, Bool)? in
return postbox.transaction { transaction -> (Peer, [MessageReaction.Reaction]?, Bool, Bool)? in
guard let peer = transaction.getPeer(messageId.peerId) else {
return nil
}
@ -115,20 +117,22 @@ private func requestUpdateMessageReaction(postbox: Postbox, network: Network, st
}
var reactions: [MessageReaction.Reaction]?
var isLarge: Bool = false
var storeAsRecentlyUsed: Bool = false
for attribute in message.attributes {
if let attribute = attribute as? PendingReactionsMessageAttribute {
if !attribute.reactions.isEmpty {
reactions = attribute.reactions.map(\.value)
}
isLarge = attribute.isLarge
storeAsRecentlyUsed = attribute.storeAsRecentlyUsed
break
}
}
return (peer, reactions, isLarge)
return (peer, reactions, isLarge, storeAsRecentlyUsed)
}
|> castError(RequestUpdateMessageReactionError.self)
|> mapToSignal { peerAndValue in
guard let (peer, reactions, isLarge) = peerAndValue else {
guard let (peer, reactions, isLarge, storeAsRecentlyUsed) = peerAndValue else {
return .fail(.generic)
}
guard let inputPeer = apiInputPeer(peer) else {
@ -144,6 +148,9 @@ private func requestUpdateMessageReaction(postbox: Postbox, network: Network, st
if isLarge {
flags |= 1 << 1
}
if storeAsRecentlyUsed {
flags |= 1 << 2
}
}
let signal: Signal<Never, RequestUpdateMessageReactionError> = network.request(Api.functions.messages.sendReaction(flags: flags, peer: inputPeer, msgId: messageId.id, reaction: reactions?.map(\.apiReaction)))
@ -518,16 +525,23 @@ public final class EngineMessageReactionListContext {
guard let strongSelf = self else {
return
}
var existingPeerIds = Set<EnginePeer.Id>()
struct ItemHash: Hashable {
var peerId: EnginePeer.Id
var value: MessageReaction.Reaction?
}
var existingItems = Set<ItemHash>()
for item in strongSelf.state.items {
existingPeerIds.insert(item.peer.id)
existingItems.insert(ItemHash(peerId: item.peer.id, value: item.reaction))
}
for item in state.items {
if existingPeerIds.contains(item.peer.id) {
let itemHash = ItemHash(peerId: item.peer.id, value: item.reaction)
if existingItems.contains(itemHash) {
continue
}
existingPeerIds.insert(item.peer.id)
existingItems.insert(itemHash)
strongSelf.state.items.append(item)
}
if state.canLoadMore {

View File

@ -250,6 +250,7 @@ public final class PendingReactionsMessageAttribute: MessageAttribute {
public let accountPeerId: PeerId?
public let reactions: [PendingReaction]
public let isLarge: Bool
public let storeAsRecentlyUsed: Bool
public var associatedPeerIds: [PeerId] {
if let accountPeerId = self.accountPeerId {
@ -277,16 +278,18 @@ public final class PendingReactionsMessageAttribute: MessageAttribute {
return result
}
public init(accountPeerId: PeerId?, reactions: [PendingReaction], isLarge: Bool) {
public init(accountPeerId: PeerId?, reactions: [PendingReaction], isLarge: Bool, storeAsRecentlyUsed: Bool) {
self.accountPeerId = accountPeerId
self.reactions = reactions
self.isLarge = isLarge
self.storeAsRecentlyUsed = storeAsRecentlyUsed
}
required public init(decoder: PostboxDecoder) {
self.accountPeerId = decoder.decodeOptionalInt64ForKey("ap").flatMap(PeerId.init)
self.reactions = decoder.decodeObjectArrayWithDecoderForKey("reac")
self.isLarge = decoder.decodeInt32ForKey("l", orElse: 0) != 0
self.storeAsRecentlyUsed = decoder.decodeInt32ForKey("used", orElse: 0) != 0
}
public func encode(_ encoder: PostboxEncoder) {
@ -299,5 +302,6 @@ public final class PendingReactionsMessageAttribute: MessageAttribute {
encoder.encodeObjectArray(self.reactions, forKey: "reac")
encoder.encodeInt32(self.isLarge ? 1 : 0, forKey: "l")
encoder.encodeInt32(self.storeAsRecentlyUsed ? 1 : 0, forKey: "used")
}
}

View File

@ -522,6 +522,15 @@ public final class TelegramMediaFile: Media, Equatable, Codable {
return false
}
public var isStaticEmoji: Bool {
for attribute in self.attributes {
if case .CustomEmoji = attribute {
return self.mimeType == "image/webp"
}
}
return false
}
public var isVideo: Bool {
for attribute in self.attributes {
if case .Video = attribute {

View File

@ -140,6 +140,18 @@ public extension TelegramEngine {
|> ignoreValues
}
public func clearRecentlyUsedReactions() -> Signal<Never, NoError> {
let _ = self.account.postbox.transaction({ transaction -> Void in
transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.CloudRecentReactions, items: [])
}).start()
return self.account.network.request(Api.functions.messages.clearRecentReactions())
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
}
public func reorderStickerPacks(namespace: ItemCollectionId.Namespace, itemIds: [ItemCollectionId]) -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
let infos = transaction.getItemCollectionsInfos(namespace: namespace)

View File

@ -19,10 +19,14 @@ swift_library(
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
"//submodules/Components/PagerComponent:PagerComponent",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AccountContext:AccountContext",
"//submodules/lottie-ios:Lottie",
"//submodules/TextFormat:TextFormat",
"//submodules/AppBundle:AppBundle",
],
visibility = [
"//visibility:public",

View File

@ -13,6 +13,10 @@ import AccountContext
import PagerComponent
import Postbox
import TelegramCore
import Lottie
import EmojiTextAttachmentView
import TextFormat
import AppBundle
public final class EmojiStatusSelectionComponent: Component {
public typealias EnvironmentType = Empty
@ -184,6 +188,11 @@ public final class EmojiStatusSelectionController: ViewController {
private var emojiContent: EmojiPagerContentComponent?
private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation?
private var availableReactions: AvailableReactions?
private var availableReactionsDisposable: Disposable?
private var hapticFeedback: HapticFeedback?
private var isDismissed: Bool = false
init(controller: EmojiStatusSelectionController, context: AccountContext, sourceView: UIView?, emojiContent: Signal<EmojiPagerContentComponent, NoError>) {
@ -235,11 +244,11 @@ public final class EmojiStatusSelectionController: ViewController {
strongSelf.emojiContent = emojiContent
emojiContent.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { _, item, _, _, _, _ in
performItemAction: { groupId, item, _, _, _, _ in
guard let strongSelf = self else {
return
}
strongSelf.applyItem(item: item)
strongSelf.applyItem(groupId: groupId, item: item)
},
deleteBackwards: {
},
@ -291,10 +300,20 @@ public final class EmojiStatusSelectionController: ViewController {
strongSelf.refreshLayout(transition: .immediate)
})
self.availableReactionsDisposable = (context.engine.stickers.availableReactions()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] availableReactions in
guard let strongSelf = self else {
return
}
strongSelf.availableReactions = availableReactions
})
}
deinit {
self.emojiContentDisposable?.dispose()
self.availableReactionsDisposable?.dispose()
}
private func refreshLayout(transition: Transition) {
@ -316,6 +335,134 @@ public final class EmojiStatusSelectionController: ViewController {
self.cloudShadowLayer1.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
}
func animateOutToStatus(groupId: AnyHashable, item: EmojiPagerContentComponent.Item, destinationView: UIView) {
guard let emojiView = self.componentHost.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View, let sourceLayer = emojiView.layerForItem( groupId: groupId, item: item) else {
self.controller?.dismiss()
return
}
self.isUserInteractionEnabled = false
let hapticFeedback: HapticFeedback
if let current = self.hapticFeedback {
hapticFeedback = current
} else {
hapticFeedback = HapticFeedback()
self.hapticFeedback = hapticFeedback
}
hapticFeedback.prepareTap()
var itemCompleted = false
var contentCompleted = false
var effectCompleted = false
let completion: () -> Void = { [weak self] in
guard let strongSelf = self, itemCompleted, contentCompleted, effectCompleted else {
return
}
strongSelf.controller?.dismissNow()
}
if let sourceCopyLayer = sourceLayer.snapshotContentTree() {
self.layer.addSublayer(sourceCopyLayer)
sourceCopyLayer.frame = sourceLayer.convert(sourceLayer.bounds, to: self.layer)
sourceLayer.isHidden = true
destinationView.isHidden = true
let previousSourceCopyFrame = sourceCopyLayer.frame
let localDestinationFrame = destinationView.convert(destinationView.bounds, to: self.view)
let destinationSize = max(localDestinationFrame.width, localDestinationFrame.height)
let effectFrame = localDestinationFrame.insetBy(dx: -destinationSize * 2.0, dy: -destinationSize * 2.0)
let destinationNormalScale = localDestinationFrame.width / previousSourceCopyFrame.width
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear)
sourceCopyLayer.position = localDestinationFrame.center
transition.animatePositionWithKeyframes(layer: sourceCopyLayer, keyframes: generateParabollicMotionKeyframes(from: previousSourceCopyFrame.center, to: localDestinationFrame.center, elevation: -(localDestinationFrame.center.y - previousSourceCopyFrame.center.y) + 30.0), completion: { [weak self, weak sourceCopyLayer, weak destinationView] _ in
guard let strongSelf = self else {
return
}
itemCompleted = true
sourceCopyLayer?.isHidden = true
if let destinationView = destinationView {
destinationView.isHidden = false
destinationView.layer.animateScale(from: 0.5, to: 1.0, duration: 0.1)
}
hapticFeedback.tap()
if let itemFile = item.itemFile, let url = getAppBundle().url(forResource: "generic_reaction_small_effect", withExtension: "json"), let composition = Animation.filepath(url.path) {
let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
view.animationSpeed = 1.0
view.backgroundColor = nil
view.isOpaque = false
let animationCache = strongSelf.context.animationCache
let animationRenderer = strongSelf.context.animationRenderer
for i in 1 ... 7 {
let allLayers = view.allLayers(forKeypath: AnimationKeypath(keypath: "placeholder_\(i)"))
for animationLayer in allLayers {
let baseItemLayer = InlineStickerItemLayer(
context: strongSelf.context,
attemptSynchronousLoad: false,
emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: nil, fileId: itemFile.fileId.id, file: itemFile),
file: item.itemFile,
cache: animationCache,
renderer: animationRenderer,
placeholderColor: UIColor(white: 0.0, alpha: 0.0),
pointSize: CGSize(width: 32.0, height: 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)
}
}
view.frame = effectFrame
strongSelf.view.addSubview(view)
view.play(completion: { _ in
effectCompleted = true
completion()
})
} else {
effectCompleted = true
}
completion()
})
let scaleKeyframes: [CGFloat] = [
1.0,
1.4,
1.0,
destinationNormalScale * 0.5
]
sourceCopyLayer.transform = CATransform3DMakeScale(scaleKeyframes[scaleKeyframes.count - 1], scaleKeyframes[scaleKeyframes.count - 1], 1.0)
sourceCopyLayer.animateKeyframes(values: scaleKeyframes.map({ $0 as NSNumber }), duration: 0.2, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.linear.rawValue)
} else {
itemCompleted = true
}
/*if let availableReactions = self.availableReactions, let availableReaction = availableReactions.reactions.first(where: { $0.value == }) {
} else {
effectCompleted = true
}*/
self.animateOut(completion: {
contentCompleted = true
completion()
})
}
func containerLayoutUpdated(layout: ContainerViewLayout, transition: Transition) {
self.validLayout = layout
@ -465,30 +612,42 @@ public final class EmojiStatusSelectionController: ViewController {
return nil
}
private func applyItem(item: EmojiPagerContentComponent.Item?) {
self.controller?.dismiss()
private func applyItem(groupId: AnyHashable, item: EmojiPagerContentComponent.Item?) {
guard let controller = self.controller else {
return
}
let _ = (self.context.engine.accountData.setEmojiStatus(file: item?.itemFile)
|> deliverOnMainQueue).start()
if let item = item, let destinationView = controller.destinationItemView() {
self.animateOutToStatus(groupId: groupId, item: item, destinationView: destinationView)
} else {
controller.dismiss()
}
}
}
private let context: AccountContext
private weak var sourceView: UIView?
private let emojiContent: Signal<EmojiPagerContentComponent, NoError>
private let destinationItemView: () -> UIView?
fileprivate let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
public init(context: AccountContext, sourceView: UIView, emojiContent: Signal<EmojiPagerContentComponent, NoError>) {
public init(context: AccountContext, sourceView: UIView, emojiContent: Signal<EmojiPagerContentComponent, NoError>, destinationItemView: @escaping () -> UIView?) {
self.context = context
self.sourceView = sourceView
self.emojiContent = emojiContent
self.destinationItemView = destinationItemView
super.init(navigationBarPresentationData: nil)
self.lockOrientation = true
self.statusBar.statusBarStyle = .Ignore
}
@ -500,6 +659,10 @@ public final class EmojiStatusSelectionController: ViewController {
super.viewDidAppear(animated)
}
private func dismissNow() {
self.presentingViewController?.dismiss(animated: false, completion: nil)
}
override public func dismiss(completion: (() -> Void)? = nil) {
(self.displayNode as! Node).animateOut(completion: { [weak self] in
self?.presentingViewController?.dismiss(animated: false, completion: nil)
@ -519,3 +682,37 @@ public final class EmojiStatusSelectionController: ViewController {
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition))
}
}
private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat) -> [CGPoint] {
let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - elevation)
let x1 = sourcePoint.x
let y1 = sourcePoint.y
let x2 = midPoint.x
let y2 = midPoint.y
let x3 = targetPosition.x
let y3 = targetPosition.y
var keyframes: [CGPoint] = []
if abs(y1 - y3) < 5.0 && abs(x1 - x3) < 5.0 {
for i in 0 ..< 10 {
let k = CGFloat(i) / CGFloat(10 - 1)
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
let y = sourcePoint.y * (1.0 - k) + targetPosition.y * k
keyframes.append(CGPoint(x: x, y: y))
}
} else {
let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
for i in 0 ..< 10 {
let k = CGFloat(i) / CGFloat(10 - 1)
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
let y = a * x * x + b * x + c
keyframes.append(CGPoint(x: x, y: y))
}
}
return keyframes
}

View File

@ -2728,6 +2728,15 @@ public final class EmojiPagerContentComponent: Component {
}
}
public func layerForItem(groupId: AnyHashable, item: EmojiPagerContentComponent.Item) -> CALayer? {
let itemKey = EmojiPagerContentComponent.View.ItemLayer.Key(groupId: groupId, itemId: item.content.id)
if let itemLayer = self.visibleItemLayers[itemKey] {
return itemLayer
} else {
return nil
}
}
public func scrollToItemGroup(id supergroupId: AnyHashable, subgroupId: Int32?) {
guard let component = self.component, let pagerEnvironment = self.pagerEnvironment, let itemLayout = self.itemLayout else {
return
@ -4593,6 +4602,7 @@ public final class EmojiPagerContentComponent: Component {
var isPremiumLocked: Bool
var isFeatured: Bool
var isExpandable: Bool
var isClearable: Bool
var headerItem: EntityKeyboardAnimationData?
var items: [EmojiPagerContentComponent.Item]
}
@ -4628,7 +4638,7 @@ public final class EmojiPagerContentComponent: Component {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: true, headerItem: nil, items: [resultItem]))
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: true, isClearable: false, headerItem: nil, items: [resultItem]))
}
var existingIds = Set<MediaId>()
@ -4706,56 +4716,107 @@ public final class EmojiPagerContentComponent: Component {
let groupId = "recent"
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
if itemGroups[groupIndex].items.count >= 16 {
break
}
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem]))
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, isClearable: false, headerItem: nil, items: [resultItem]))
}
}
if let recentReactions = recentReactions {
for item in recentReactions.items {
guard let item = item.contents.get(RecentReactionItem.self) else {
continue
}
let animationFile: TelegramMediaFile
switch item.content {
case let .builtin(value):
if existingIds.contains(.builtin(value)) {
if hasPremium {
var hasRecent = false
if let recentReactions = recentReactions, !recentReactions.items.isEmpty {
hasRecent = true
}
let maxRecentLineCount = 4
//TODO:localize
let popularTitle = hasRecent ? "Recently Used" : "Popular"
if let availableReactions = availableReactions {
for reactionItem in availableReactions.reactions {
if !reactionItem.isEnabled {
continue
}
existingIds.insert(.builtin(value))
if let availableReactions = availableReactions, let availableReaction = availableReactions.reactions.first(where: { $0.value == .builtin(value) }) {
if let centerAnimation = availableReaction.centerAnimation {
animationFile = centerAnimation
if existingIds.contains(reactionItem.value) {
continue
}
existingIds.insert(reactionItem.value)
let animationFile = reactionItem.selectAnimation
let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true)
let resultItem = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: animationFile,
subgroupId: nil
)
let groupId = "popular"
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, isClearable: hasRecent, headerItem: nil, items: [resultItem]))
}
}
}
if let recentReactions = recentReactions {
var popularInsertIndex = 0
for item in recentReactions.items {
guard let item = item.contents.get(RecentReactionItem.self) else {
continue
}
let animationFile: TelegramMediaFile
switch item.content {
case let .builtin(value):
if existingIds.contains(.builtin(value)) {
continue
}
existingIds.insert(.builtin(value))
if let availableReactions = availableReactions, let availableReaction = availableReactions.reactions.first(where: { $0.value == .builtin(value) }) {
if let centerAnimation = availableReaction.centerAnimation {
animationFile = centerAnimation
} else {
continue
}
} else {
continue
}
case let .custom(file):
if existingIds.contains(.custom(file.fileId.id)) {
continue
}
existingIds.insert(.custom(file.fileId.id))
animationFile = file
}
let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true)
let resultItem = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: animationFile,
subgroupId: nil
)
let groupId = "popular"
if let groupIndex = itemGroupIndexById[groupId] {
if itemGroups[groupIndex].items.count + 1 >= maxRecentLineCount * 8 {
break
}
itemGroups[groupIndex].items.insert(resultItem, at: popularInsertIndex)
popularInsertIndex += 1
} else {
continue
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, isClearable: hasRecent, headerItem: nil, items: [resultItem]))
}
case let .custom(file):
if existingIds.contains(.custom(file.fileId.id)) {
continue
}
existingIds.insert(.custom(file.fileId.id))
animationFile = file
}
let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true)
let resultItem = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: animationFile,
subgroupId: nil
)
let groupId = "recent"
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem]))
}
}
}
@ -4799,7 +4860,7 @@ public final class EmojiPagerContentComponent: Component {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.Emoji_FrequentlyUsed, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem]))
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.Emoji_FrequentlyUsed, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, isClearable: true, headerItem: nil, items: [resultItem]))
}
}
}
@ -4819,7 +4880,7 @@ public final class EmojiPagerContentComponent: Component {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitleEmoji, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem]))
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitleEmoji, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, isClearable: false, headerItem: nil, items: [resultItem]))
}
}
}
@ -4883,7 +4944,7 @@ public final class EmojiPagerContentComponent: Component {
break inner
}
}
itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: false, isExpandable: false, headerItem: headerItem, items: [resultItem]))
itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: false, isExpandable: false, isClearable: false, headerItem: headerItem, items: [resultItem]))
}
}
@ -4937,7 +4998,7 @@ public final class EmojiPagerContentComponent: Component {
)
}
itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: featuredEmojiPack.info.title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: true, isExpandable: true, headerItem: headerItem, items: [resultItem]))
itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: featuredEmojiPack.info.title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: true, isExpandable: true, isClearable: false, headerItem: headerItem, items: [resultItem]))
}
}
}
@ -4952,11 +5013,6 @@ public final class EmojiPagerContentComponent: Component {
animationRenderer: animationRenderer,
inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(),
itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
var hasClear = false
if group.id == AnyHashable("recent") {
hasClear = true
}
var headerItem = group.headerItem
if let groupId = group.id.base as? ItemCollectionId {
@ -4983,7 +5039,7 @@ public final class EmojiPagerContentComponent: Component {
isFeatured: group.isFeatured,
isPremiumLocked: group.isPremiumLocked,
isEmbedded: false,
hasClear: hasClear,
hasClear: group.isClearable,
isExpandable: group.isExpandable,
displayPremiumBadges: false,
headerItem: headerItem,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -19,6 +19,8 @@ import PresentationDataUtils
import MeshAnimationCache
import FetchManagerImpl
import InAppPurchaseManager
import AnimationCache
import MultiAnimationRenderer
private final class DeviceSpecificContactImportContext {
let disposable = MetaDisposable()
@ -160,6 +162,9 @@ public final class AccountContextImpl: AccountContext {
public let cachedGroupCallContexts: AccountGroupCallContextCache
public let meshAnimationCache: MeshAnimationCache
public let animationCache: AnimationCache
public let animationRenderer: MultiAnimationRenderer
private var animatedEmojiStickersDisposable: Disposable?
public private(set) var animatedEmojiStickers: [String: [StickerPackItem]] = [:]
@ -204,6 +209,11 @@ public final class AccountContextImpl: AccountContext {
self.cachedGroupCallContexts = AccountGroupCallContextCacheImpl()
self.meshAnimationCache = MeshAnimationCache(mediaBox: account.postbox.mediaBox)
self.animationCache = AnimationCacheImpl(basePath: self.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationRenderer = MultiAnimationRendererImpl()
let updatedLimitsConfiguration = account.postbox.preferencesView(keys: [PreferencesKeys.limitsConfiguration])
|> map { preferences -> LimitsConfiguration in
return preferences.values[PreferencesKeys.limitsConfiguration]?.get(LimitsConfiguration.self) ?? LimitsConfiguration.defaultValue

View File

@ -1336,7 +1336,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageId: message.id, reactions: mappedUpdatedReactions, isLarge: isLarge).start()
let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageId: message.id, reactions: mappedUpdatedReactions, isLarge: isLarge, storeAsRecentlyUsed: true).start()
/*let currentReactions = mergedMessageReactions(attributes: message.attributes)?.reactions ?? []
var updatedReactions: [MessageReaction.Reaction] = currentReactions.filter(\.isSelected).map(\.value)
@ -1743,7 +1743,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageId: message.id, reactions: mappedUpdatedReactions, isLarge: false).start()
let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageId: message.id, reactions: mappedUpdatedReactions, isLarge: false, storeAsRecentlyUsed: false).start()
}
})
}, activateMessagePinch: { [weak self] sourceNode in

View File

@ -87,15 +87,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
let animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
let animationRenderer: MultiAnimationRenderer
/*if #available(iOS 13.0, *) {
animationRenderer = MultiAnimationMetalRendererImpl()
} else {*/
animationRenderer = MultiAnimationRendererImpl()
//}
let animationCache = context.animationCache
let animationRenderer = context.animationRenderer
let emojiItems = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, isStatusSelection: false, isReactionSelection: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: chatPeerId)
@ -1902,10 +1895,8 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV
) {
self.context = context
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationRenderer = MultiAnimationRendererImpl()
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
if isDark {

View File

@ -2410,6 +2410,8 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus
}
if let currentStats = self.currentStats {
reactionCount = currentStats.reactionCount
if currentStats.peers.isEmpty {
if reactionCount != 0 {
let text: String = self.presentationData.strings.Chat_ContextReactionCount(Int32(reactionCount))

View File

@ -298,18 +298,8 @@ class ChatPresentationContext {
init(context: AccountContext, backgroundNode: WallpaperBackgroundNode?) {
self.backgroundNode = backgroundNode
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
let animationRenderer: MultiAnimationRenderer
/*if #available(iOS 13.0, *) {
animationRenderer = MultiAnimationMetalRendererImpl()
} else {*/
animationRenderer = MultiAnimationRendererImpl()
//}
self.animationRenderer = animationRenderer
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
}
}

View File

@ -113,12 +113,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
func setupItem(_ item: ChatMessageNotificationItem, compact: Bool) {
self.item = item
if self.animationCache == nil {
self.animationCache = AnimationCacheImpl(basePath: item.context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.multiAnimationRenderer = MultiAnimationRendererImpl()
}
self.animationCache = item.context.animationCache
self.compact = compact
if compact {

View File

@ -1411,10 +1411,20 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
var entities: [MessageTextEntity] = []
let rawText: PresentationStrings.FormattedString
if !updatedValue.isEmpty {
let emojiString = updatedValue.joined(separator: ", ")
switch updatedValue {
case .all:
rawText = self.presentationData.strings.Channel_AdminLog_ReactionsEnabled(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "")
case let .limited(reactions):
let emojiString = reactions.compactMap({ reaction -> String? in
switch reaction {
case let .builtin(value):
return value
case .custom:
return nil
}
}).joined(separator: ", ")
rawText = self.presentationData.strings.Channel_AdminLog_AllowedReactionsUpdated(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", emojiString)
} else {
case .empty:
rawText = self.presentationData.strings.Channel_AdminLog_ReactionsDisabled(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "")
}

View File

@ -173,10 +173,8 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe
self.presentationData = presentationData
self.presentationDataPromise = Promise(ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true))
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationRenderer = MultiAnimationRendererImpl()
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
self.listNode = ListView()
self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor

View File

@ -77,10 +77,8 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
self.context = context
self.presentationData = presentationData
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationRenderer = MultiAnimationRendererImpl()
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
var placeholder: String
var includeChatList = false

View File

@ -2109,10 +2109,8 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationRenderer = MultiAnimationRendererImpl()
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
super.init()
@ -2676,7 +2674,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
collapsedTransitionOffset = -10.0 * navigationTransition.fraction
}
transition.updateFrame(view: self.titleCredibilityIconView, frame: CGRect(origin: CGPoint(x: titleSize.width + 4.0 + collapsedTransitionOffset, y: floor((titleSize.height - credibilityIconSize.height) / 2.0)), size: credibilityIconSize))
transition.updateFrame(view: self.titleCredibilityIconView, frame: CGRect(origin: CGPoint(x: titleSize.width + 4.0 + collapsedTransitionOffset, y: floor((titleSize.height - credibilityIconSize.height) / 2.0) + 2.0), size: credibilityIconSize))
transition.updateFrame(view: self.titleExpandedCredibilityIconView, frame: CGRect(origin: CGPoint(x: titleExpandedSize.width + 4.0, y: floor((titleExpandedSize.height - titleExpandedCredibilityIconSize.height) / 2.0) + 1.0), size: titleExpandedCredibilityIconSize))
}
@ -3155,9 +3153,6 @@ final class PeerInfoHeaderNode: ASDisplayNode {
guard let result = super.hitTest(point, with: event) else {
return nil
}
if result.isDescendant(of: self.navigationButtonContainer.view) {
return result
}
if !self.backgroundNode.frame.contains(point) {
return nil
}
@ -3177,6 +3172,10 @@ final class PeerInfoHeaderNode: ASDisplayNode {
}
}
if result.isDescendant(of: self.navigationButtonContainer.view) {
return result
}
if result == self.view || result == self.regularContentNode.view || result == self.editingContentNode.view {
return nil
}

View File

@ -624,16 +624,19 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
displaySetPhoto = true
}
//TODO:localize
var setStatusTitle: String = ""
let displaySetStatus: Bool
var hasEmojiStatus = false
if let peer = data.peer as? TelegramUser, peer.isPremium {
if peer.emojiStatus != nil {
hasEmojiStatus = true
//TODO:localize
setStatusTitle = "Change Emoji Status"
} else {
//TODO:localize
setStatusTitle = "Set Emoji Status"
}
displaySetStatus = true
setStatusTitle = "Set Emoji Status"
} else {
displaySetStatus = false
}
@ -972,7 +975,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
}))
}
}
if let reactionSourceMessageId = reactionSourceMessageId {
if let reactionSourceMessageId = reactionSourceMessageId, !data.isContact {
items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 3, text: presentationData.strings.UserInfo_SendMessage, action: {
interaction.openChat()
}))
@ -3089,10 +3092,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
return
}
let animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
let animationRenderer = MultiAnimationRendererImpl()
let animationCache = context.animationCache
let animationRenderer = context.animationRenderer
strongSelf.controller?.present(EmojiStatusSelectionController(
context: strongSelf.context,
sourceView: sourceView,
@ -3107,7 +3109,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
areUnicodeEmojiEnabled: false,
areCustomEmojiEnabled: true,
chatPeerId: strongSelf.context.account.peerId
)
),
destinationItemView: { [weak sourceView] in
return sourceView
}
), in: .window(.root))
}
} else {
@ -3128,20 +3133,18 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
guard let strongSelf = self else {
return
}
if let emojiStatusFile = emojiStatusFile {
let source: PremiumSource
if let peerStatus = peerStatus {
source = .emojiStatus(strongSelf.peerId, peerStatus.fileId, emojiStatusFile, emojiPackTitle)
} else {
source = .profile(strongSelf.peerId)
}
let controller = PremiumIntroScreen(context: strongSelf.context, source: source)
controller.sourceView = sourceView
controller.containerView = strongSelf.controller?.navigationController?.view
controller.animationColor = white ? .white : strongSelf.presentationData.theme.list.itemAccentColor
strongSelf.controller?.push(controller)
let source: PremiumSource
if let peerStatus = peerStatus, let emojiStatusFile = emojiStatusFile {
source = .emojiStatus(strongSelf.peerId, peerStatus.fileId, emojiStatusFile, emojiPackTitle)
} else {
source = .profile(strongSelf.peerId)
}
let controller = PremiumIntroScreen(context: strongSelf.context, source: source)
controller.sourceView = sourceView
controller.containerView = strongSelf.controller?.navigationController?.view
controller.animationColor = white ? .white : strongSelf.presentationData.theme.list.itemAccentColor
strongSelf.controller?.push(controller)
})
}

View File

@ -98,10 +98,8 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.presentationData = presentationData
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationRenderer = MultiAnimationRendererImpl()
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(id: PeerId(0)), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil)

View File

@ -16,15 +16,37 @@ func topMessageReactions(context: AccountContext, message: Message) -> Signal<[R
return item.contents.get(RecentReactionItem.self)
}
}
let allowedReactionsWithFiles: Signal<(reactions: AllowedReactions, files: [Int64: TelegramMediaFile])?, NoError> = peerMessageAllowedReactions(context: context, message: message)
|> mapToSignal { allowedReactions -> Signal<(reactions: AllowedReactions, files: [Int64: TelegramMediaFile])?, NoError> in
guard let allowedReactions = allowedReactions else {
return .single(nil)
}
if case let .set(reactions) = allowedReactions {
return context.engine.stickers.resolveInlineStickers(fileIds: reactions.compactMap { item -> Int64? in
switch item {
case .builtin:
return nil
case let .custom(fileId):
return fileId
}
})
|> map { files -> (reactions: AllowedReactions, files: [Int64: TelegramMediaFile]) in
return (allowedReactions, files)
}
} else {
return .single((allowedReactions, [:]))
}
}
return combineLatest(
context.engine.stickers.availableReactions(),
peerMessageAllowedReactions(context: context, message: message),
allowedReactionsWithFiles,
topReactions
)
|> take(1)
|> map { availableReactions, allowedReactions, topReactions -> [ReactionItem] in
guard let availableReactions = availableReactions, let allowedReactions = allowedReactions else {
|> map { availableReactions, allowedReactionsAndFiles, topReactions -> [ReactionItem] in
guard let availableReactions = availableReactions, let allowedReactionsAndFiles = allowedReactionsAndFiles else {
return []
}
@ -48,7 +70,7 @@ func topMessageReactions(context: AccountContext, message: Message) -> Signal<[R
}
existingIds.insert(reaction.value)
switch allowedReactions {
switch allowedReactionsAndFiles.reactions {
case let .set(set):
if !set.contains(reaction.value) {
continue
@ -71,7 +93,7 @@ func topMessageReactions(context: AccountContext, message: Message) -> Signal<[R
continue
}
case let .custom(file):
switch allowedReactions {
switch allowedReactionsAndFiles.reactions {
case let .set(set):
if !set.contains(.custom(file.fileId.id)) {
continue
@ -109,13 +131,13 @@ func topMessageReactions(context: AccountContext, message: Message) -> Signal<[R
continue
}
switch allowedReactions {
switch allowedReactionsAndFiles.reactions {
case let .set(set):
if !set.contains(reaction.value) {
continue
}
case .all:
break
continue
}
if existingIds.contains(reaction.value) {
@ -134,6 +156,33 @@ func topMessageReactions(context: AccountContext, message: Message) -> Signal<[R
isCustom: false
))
}
if case let .set(reactions) = allowedReactionsAndFiles.reactions {
for reaction in reactions {
if existingIds.contains(reaction) {
continue
}
existingIds.insert(reaction)
switch reaction {
case .builtin:
break
case let .custom(fileId):
if let file = allowedReactionsAndFiles.files[fileId] {
result.append(ReactionItem(
reaction: ReactionItem.Reaction(rawValue: .custom(file.fileId.id)),
appearAnimation: file,
stillAnimation: file,
listAnimation: file,
largeListAnimation: file,
applicationAnimation: nil,
largeApplicationAnimation: nil,
isCustom: true
))
}
}
}
}
return result
}