Emoji status and reaction improvements

This commit is contained in:
Ali 2022-08-30 18:38:47 +04:00
parent 5ca7417ee1
commit b924ea326e
48 changed files with 1386 additions and 413 deletions

View File

@ -118,7 +118,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
case .premium: case .premium:
statusContent = .premium(color: self.theme.list.itemAccentColor) statusContent = .premium(color: self.theme.list.itemAccentColor)
case let .emoji(emoji): case let .emoji(emoji):
statusContent = .animation(content: .customEmoji(fileId: emoji.fileId), size: CGSize(width: 22.0, height: 22.0), placeholderColor: self.theme.list.mediaPlaceholderColor) statusContent = .animation(content: .customEmoji(fileId: emoji.fileId), size: CGSize(width: 22.0, height: 22.0), placeholderColor: self.theme.list.mediaPlaceholderColor, themeColor: self.theme.list.itemAccentColor, loopMode: .count(2))
} }
var titleCredibilityIconTransition: Transition var titleCredibilityIconTransition: Transition
@ -144,6 +144,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
animationCache: self.animationCache, animationCache: self.animationCache,
animationRenderer: self.animationRenderer, animationRenderer: self.animationRenderer,
content: statusContent, content: statusContent,
isVisibleForAnimations: true,
action: { [weak self] in action: { [weak self] in
guard let strongSelf = self, let titleCredibilityIconView = strongSelf.titleCredibilityIconView else { guard let strongSelf = self, let titleCredibilityIconView = strongSelf.titleCredibilityIconView else {
return return
@ -351,7 +352,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
case .premium: case .premium:
statusContent = .premium(color: self.theme.list.itemAccentColor) statusContent = .premium(color: self.theme.list.itemAccentColor)
case let .emoji(emoji): case let .emoji(emoji):
statusContent = .animation(content: .customEmoji(fileId: emoji.fileId), size: CGSize(width: 22.0, height: 22.0), placeholderColor: self.theme.list.mediaPlaceholderColor) statusContent = .animation(content: .customEmoji(fileId: emoji.fileId), size: CGSize(width: 22.0, height: 22.0), placeholderColor: self.theme.list.mediaPlaceholderColor, themeColor: self.theme.list.itemAccentColor, loopMode: .count(2))
} }
var titleCredibilityIconTransition = Transition(transition) var titleCredibilityIconTransition = Transition(transition)
@ -372,6 +373,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
animationCache: self.animationCache, animationCache: self.animationCache,
animationRenderer: self.animationRenderer, animationRenderer: self.animationRenderer,
content: statusContent, content: statusContent,
isVisibleForAnimations: true,
action: { [weak self] in action: { [weak self] in
guard let strongSelf = self, let titleCredibilityIconView = strongSelf.titleCredibilityIconView else { guard let strongSelf = self, let titleCredibilityIconView = strongSelf.titleCredibilityIconView else {
return return

View File

@ -460,6 +460,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let pinnedIconNode: ASImageNode let pinnedIconNode: ASImageNode
var secretIconNode: ASImageNode? var secretIconNode: ASImageNode?
var credibilityIconView: ComponentHostView<Empty>? var credibilityIconView: ComponentHostView<Empty>?
var credibilityIconComponent: EmojiStatusComponent?
let mutedIconNode: ASImageNode let mutedIconNode: ASImageNode
private var hierarchyTrackingLayer: HierarchyTrackingLayer? private var hierarchyTrackingLayer: HierarchyTrackingLayer?
@ -634,6 +635,15 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
self.updateVideoVisibility() self.updateVideoVisibility()
self.textNode.visibilityRect = self.visibilityStatus ? CGRect.infinite : nil self.textNode.visibilityRect = self.visibilityStatus ? CGRect.infinite : nil
if let credibilityIconView = self.credibilityIconView, let credibilityIconComponent = self.credibilityIconComponent {
let _ = credibilityIconView.update(
transition: .immediate,
component: AnyComponent(credibilityIconComponent.withVisibleForAnimations(self.visibilityStatus)),
environment: {},
containerSize: credibilityIconView.bounds.size
)
}
} }
} }
} }
@ -1521,7 +1531,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
case let .peer(messages, _, _, _, _, _, _, _, _, _, _, _, _): case let .peer(messages, _, _, _, _, _, _, _, _, _, _, _, _):
if let peer = messages.last?.author { if let peer = messages.last?.author {
if case let .user(user) = peer, let emojiStatus = user.emojiStatus { if case let .user(user) = peer, let emojiStatus = user.emojiStatus {
currentCredibilityIconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor) currentCredibilityIconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2))
} else if peer.isScam { } else if peer.isScam {
currentCredibilityIconContent = .scam(color: item.presentationData.theme.chat.message.incoming.scamColor) currentCredibilityIconContent = .scam(color: item.presentationData.theme.chat.message.incoming.scamColor)
} else if peer.isFake { } else if peer.isFake {
@ -1537,7 +1547,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
} }
} else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer { } else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer {
if case let .user(user) = peer, let emojiStatus = user.emojiStatus { if case let .user(user) = peer, let emojiStatus = user.emojiStatus {
currentCredibilityIconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor) currentCredibilityIconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2))
} else if peer.isScam { } else if peer.isScam {
currentCredibilityIconContent = .scam(color: item.presentationData.theme.chat.message.incoming.scamColor) currentCredibilityIconContent = .scam(color: item.presentationData.theme.chat.message.incoming.scamColor)
} else if peer.isFake { } else if peer.isFake {
@ -2056,16 +2066,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.credibilityIconView = credibilityIconView strongSelf.credibilityIconView = credibilityIconView
strongSelf.contextContainer.view.addSubview(credibilityIconView) strongSelf.contextContainer.view.addSubview(credibilityIconView)
} }
let iconSize = credibilityIconView.update(
transition: .immediate, let credibilityIconComponent = EmojiStatusComponent(
component: AnyComponent(EmojiStatusComponent(
context: item.context, context: item.context,
animationCache: item.interaction.animationCache, animationCache: item.interaction.animationCache,
animationRenderer: item.interaction.animationRenderer, animationRenderer: item.interaction.animationRenderer,
content: currentCredibilityIconContent, content: currentCredibilityIconContent,
isVisibleForAnimations: strongSelf.visibilityStatus,
action: nil, action: nil,
longTapAction: nil longTapAction: nil
)), )
strongSelf.credibilityIconComponent = credibilityIconComponent
let iconSize = credibilityIconView.update(
transition: .immediate,
component: AnyComponent(credibilityIconComponent),
environment: {}, environment: {},
containerSize: CGSize(width: 20.0, height: 20.0) containerSize: CGSize(width: 20.0, height: 20.0)
) )

View File

@ -33,6 +33,13 @@ public final class ReactionIconView: PortalSourceView {
private var disposable: Disposable? private var disposable: Disposable?
public var iconFrame: CGRect? {
if let animationLayer = self.animationLayer {
return animationLayer.frame
}
return nil
}
override public init(frame: CGRect) { override public init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
} }

View File

@ -370,6 +370,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
private let avatarNode: AvatarNode private let avatarNode: AvatarNode
private let titleNode: TextNode private let titleNode: TextNode
private var credibilityIconView: ComponentHostView<Empty>? private var credibilityIconView: ComponentHostView<Empty>?
private var credibilityIconComponent: EmojiStatusComponent?
private let statusNode: TextNode private let statusNode: TextNode
private var badgeBackgroundNode: ASImageNode? private var badgeBackgroundNode: ASImageNode?
private var badgeTextNode: TextNode? private var badgeTextNode: TextNode?
@ -398,6 +399,37 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
return self.layoutParams?.0 return self.layoutParams?.0
} }
override public var visibility: ListViewItemNodeVisibility {
didSet {
let wasVisible = self.visibilityStatus
let isVisible: Bool
switch self.visibility {
case let .visible(fraction, _):
isVisible = fraction > 0.01
case .none:
isVisible = false
}
if wasVisible != isVisible {
self.visibilityStatus = isVisible
}
}
}
private var visibilityStatus: Bool = false {
didSet {
if self.visibilityStatus != oldValue {
if let credibilityIconView = self.credibilityIconView, let credibilityIconComponent = self.credibilityIconComponent {
let _ = credibilityIconView.update(
transition: .immediate,
component: AnyComponent(credibilityIconComponent.withVisibleForAnimations(self.visibilityStatus)),
environment: {},
containerSize: credibilityIconView.bounds.size
)
}
}
}
}
required public init() { required public init() {
self.backgroundNode = ASDisplayNode() self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true self.backgroundNode.isLayerBacked = true
@ -616,7 +648,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
} else if peer.isFake { } else if peer.isFake {
credibilityIcon = .fake(color: item.presentationData.theme.chat.message.incoming.scamColor) credibilityIcon = .fake(color: item.presentationData.theme.chat.message.incoming.scamColor)
} else if case let .user(user) = peer, let emojiStatus = user.emojiStatus { } else if case let .user(user) = peer, let emojiStatus = user.emojiStatus {
credibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor) credibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2))
} else if peer.isVerified { } else if peer.isVerified {
credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
} else if peer.isPremium && !premiumConfiguration.isPremiumDisabled { } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled {
@ -987,17 +1019,21 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
strongSelf.credibilityIconView = credibilityIconView strongSelf.credibilityIconView = credibilityIconView
} }
let iconSize = credibilityIconView.update( let credibilityIconComponent = EmojiStatusComponent(
transition: .immediate,
component: AnyComponent(EmojiStatusComponent(
context: item.context, context: item.context,
animationCache: animationCache, animationCache: animationCache,
animationRenderer: animationRenderer, animationRenderer: animationRenderer,
content: credibilityIcon, content: credibilityIcon,
isVisibleForAnimations: strongSelf.visibilityStatus,
action: nil, action: nil,
longTapAction: nil, longTapAction: nil,
emojiFileUpdated: nil emojiFileUpdated: nil
)), )
strongSelf.credibilityIconComponent = credibilityIconComponent
let iconSize = credibilityIconView.update(
transition: .immediate,
component: AnyComponent(credibilityIconComponent),
environment: {}, environment: {},
containerSize: CGSize(width: 20.0, height: 20.0) containerSize: CGSize(width: 20.0, height: 20.0)
) )

View File

@ -412,6 +412,7 @@ private func makeSubtreeSnapshot(layer: CALayer, keepTransform: Bool = false) ->
view.layer.contentsCenter = layer.contentsCenter view.layer.contentsCenter = layer.contentsCenter
view.layer.contentsGravity = layer.contentsGravity view.layer.contentsGravity = layer.contentsGravity
view.layer.masksToBounds = layer.masksToBounds view.layer.masksToBounds = layer.masksToBounds
view.layer.layerTintColor = layer.layerTintColor
if let mask = layer.mask { if let mask = layer.mask {
if let shapeMask = mask as? CAShapeLayer { if let shapeMask = mask as? CAShapeLayer {
let maskLayer = CAShapeLayer() let maskLayer = CAShapeLayer()
@ -424,8 +425,11 @@ private func makeSubtreeSnapshot(layer: CALayer, keepTransform: Bool = false) ->
maskLayer.contentsScale = mask.contentsScale maskLayer.contentsScale = mask.contentsScale
maskLayer.contentsCenter = mask.contentsCenter maskLayer.contentsCenter = mask.contentsCenter
maskLayer.contentsGravity = mask.contentsGravity maskLayer.contentsGravity = mask.contentsGravity
maskLayer.frame = mask.frame maskLayer.transform = mask.transform
maskLayer.position = mask.position
maskLayer.bounds = mask.bounds maskLayer.bounds = mask.bounds
maskLayer.anchorPoint = mask.anchorPoint
maskLayer.layerTintColor = mask.layerTintColor
view.layer.mask = maskLayer view.layer.mask = maskLayer
} }
} }
@ -438,10 +442,17 @@ private func makeSubtreeSnapshot(layer: CALayer, keepTransform: Bool = false) ->
if keepTransform { if keepTransform {
subtree.layer.transform = sublayer.transform subtree.layer.transform = sublayer.transform
} }
subtree.frame = sublayer.frame subtree.layer.transform = sublayer.transform
subtree.bounds = sublayer.bounds subtree.layer.position = sublayer.position
subtree.layer.bounds = sublayer.bounds
subtree.layer.anchorPoint = sublayer.anchorPoint
subtree.layer.layerTintColor = sublayer.layerTintColor
if let maskLayer = subtree.layer.mask { if let maskLayer = subtree.layer.mask {
maskLayer.frame = sublayer.bounds maskLayer.transform = sublayer.transform
maskLayer.position = sublayer.position
maskLayer.bounds = sublayer.bounds
maskLayer.anchorPoint = sublayer.anchorPoint
maskLayer.layerTintColor = sublayer.layerTintColor
} }
view.addSubview(subtree) view.addSubview(subtree)
} else { } else {
@ -467,13 +478,15 @@ private func makeLayerSubtreeSnapshot(layer: CALayer) -> CALayer? {
view.masksToBounds = layer.masksToBounds view.masksToBounds = layer.masksToBounds
view.cornerRadius = layer.cornerRadius view.cornerRadius = layer.cornerRadius
view.backgroundColor = layer.backgroundColor view.backgroundColor = layer.backgroundColor
view.layerTintColor = layer.layerTintColor
if let sublayers = layer.sublayers { if let sublayers = layer.sublayers {
for sublayer in sublayers { for sublayer in sublayers {
let subtree = makeLayerSubtreeSnapshot(layer: sublayer) let subtree = makeLayerSubtreeSnapshot(layer: sublayer)
if let subtree = subtree { if let subtree = subtree {
subtree.transform = sublayer.transform subtree.transform = sublayer.transform
subtree.frame = sublayer.frame subtree.position = sublayer.position
subtree.bounds = sublayer.bounds subtree.bounds = sublayer.bounds
subtree.anchorPoint = sublayer.anchorPoint
layer.addSublayer(subtree) layer.addSublayer(subtree)
} else { } else {
return nil return nil
@ -498,13 +511,16 @@ private func makeLayerSubtreeSnapshotAsView(layer: CALayer) -> UIView? {
view.layer.masksToBounds = layer.masksToBounds view.layer.masksToBounds = layer.masksToBounds
view.layer.cornerRadius = layer.cornerRadius view.layer.cornerRadius = layer.cornerRadius
view.layer.backgroundColor = layer.backgroundColor view.layer.backgroundColor = layer.backgroundColor
view.layer.layerTintColor = layer.layerTintColor
if let sublayers = layer.sublayers { if let sublayers = layer.sublayers {
for sublayer in sublayers { for sublayer in sublayers {
let subtree = makeLayerSubtreeSnapshotAsView(layer: sublayer) let subtree = makeLayerSubtreeSnapshotAsView(layer: sublayer)
if let subtree = subtree { if let subtree = subtree {
subtree.layer.transform = sublayer.transform subtree.layer.transform = sublayer.transform
subtree.layer.frame = sublayer.frame subtree.layer.position = sublayer.position
subtree.layer.bounds = sublayer.bounds subtree.layer.bounds = sublayer.bounds
subtree.layer.anchorPoint = sublayer.anchorPoint
subtree.layer.layerTintColor = sublayer.layerTintColor
view.addSubview(subtree) view.addSubview(subtree)
} else { } else {
return nil return nil
@ -526,8 +542,9 @@ public extension UIView {
self.isHidden = true self.isHidden = true
} }
if let snapshot = snapshot { if let snapshot = snapshot {
snapshot.frame = self.frame snapshot.layer.position = self.layer.position
snapshot.bounds = self.bounds snapshot.layer.bounds = self.layer.bounds
snapshot.layer.anchorPoint = self.layer.anchorPoint
return snapshot return snapshot
} }
@ -555,6 +572,21 @@ public extension CALayer {
} }
} }
public extension CALayer {
var layerTintColor: CGColor? {
get {
if let value = self.value(forKey: "contentsMultiplyColor"), CFGetTypeID(value as CFTypeRef) == CGColor.typeID {
let result = value as! CGColor
return result
} else {
return nil
}
} set(value) {
self.setValue(value, forKey: "contentsMultiplyColor")
}
}
}
public extension CALayer { public extension CALayer {
func snapshotContentTreeAsView(unhide: Bool = false) -> UIView? { func snapshotContentTreeAsView(unhide: Bool = false) -> UIView? {
let wasHidden = self.isHidden let wasHidden = self.isHidden

View File

@ -462,6 +462,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
private let labelBadgeNode: ASImageNode private let labelBadgeNode: ASImageNode
private var labelArrowNode: ASImageNode? private var labelArrowNode: ASImageNode?
private let statusNode: TextNode private let statusNode: TextNode
private var credibilityIconComponent: EmojiStatusComponent?
private var credibilityIconView: ComponentHostView<Empty>? private var credibilityIconView: ComponentHostView<Empty>?
private var switchNode: SwitchNode? private var switchNode: SwitchNode?
private var checkNode: ASImageNode? private var checkNode: ASImageNode?
@ -475,6 +476,37 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
private var editableControlNode: ItemListEditableControlNode? private var editableControlNode: ItemListEditableControlNode?
private var reorderControlNode: ItemListEditableReorderControlNode? private var reorderControlNode: ItemListEditableReorderControlNode?
override public var visibility: ListViewItemNodeVisibility {
didSet {
let wasVisible = self.visibilityStatus
let isVisible: Bool
switch self.visibility {
case let .visible(fraction, _):
isVisible = fraction > 0.01
case .none:
isVisible = false
}
if wasVisible != isVisible {
self.visibilityStatus = isVisible
}
}
}
private var visibilityStatus: Bool = false {
didSet {
if self.visibilityStatus != oldValue {
if let credibilityIconView = self.credibilityIconView, let credibilityIconComponent = self.credibilityIconComponent {
let _ = credibilityIconView.update(
transition: .immediate,
component: AnyComponent(credibilityIconComponent.withVisibleForAnimations(self.visibilityStatus)),
environment: {},
containerSize: credibilityIconView.bounds.size
)
}
}
}
}
override public var canBeSelected: Bool { override public var canBeSelected: Bool {
if self.editableControlNode != nil || self.disabledOverlayNode != nil { if self.editableControlNode != nil || self.disabledOverlayNode != nil {
return false return false
@ -614,7 +646,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
} else if item.peer.isFake { } else if item.peer.isFake {
credibilityIcon = .fake(color: item.presentationData.theme.chat.message.incoming.scamColor) credibilityIcon = .fake(color: item.presentationData.theme.chat.message.incoming.scamColor)
} else if case let .user(user) = item.peer, let emojiStatus = user.emojiStatus { } else if case let .user(user) = item.peer, let emojiStatus = user.emojiStatus {
credibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor) credibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2))
} else if item.peer.isVerified { } else if item.peer.isVerified {
credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
} else if item.peer.isPremium && !premiumConfiguration.isPremiumDisabled { } else if item.peer.isPremium && !premiumConfiguration.isPremiumDisabled {
@ -1085,17 +1117,20 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
strongSelf.credibilityIconView = credibilityIconView strongSelf.credibilityIconView = credibilityIconView
} }
let iconSize = credibilityIconView.update( let credibilityIconComponent = EmojiStatusComponent(
transition: .immediate,
component: AnyComponent(EmojiStatusComponent(
context: item.context, context: item.context,
animationCache: animationCache, animationCache: animationCache,
animationRenderer: animationRenderer, animationRenderer: animationRenderer,
content: credibilityIcon, content: credibilityIcon,
isVisibleForAnimations: strongSelf.visibilityStatus,
action: nil, action: nil,
longTapAction: nil, longTapAction: nil,
emojiFileUpdated: nil emojiFileUpdated: nil
)), )
strongSelf.credibilityIconComponent = credibilityIconComponent
let iconSize = credibilityIconView.update(
transition: .immediate,
component: AnyComponent(credibilityIconComponent),
environment: {}, environment: {},
containerSize: CGSize(width: 20.0, height: 20.0) containerSize: CGSize(width: 20.0, height: 20.0)
) )

View File

@ -20,12 +20,12 @@ private enum PeerReactionsMode {
private final class PeerAllowedReactionListControllerArguments { private final class PeerAllowedReactionListControllerArguments {
let context: AccountContext let context: AccountContext
let setMode: (PeerReactionsMode) -> Void let setMode: (PeerReactionsMode, Bool) -> Void
let toggleItem: (MessageReaction.Reaction) -> Void let toggleItem: (MessageReaction.Reaction) -> Void
init( init(
context: AccountContext, context: AccountContext,
setMode: @escaping (PeerReactionsMode) -> Void, setMode: @escaping (PeerReactionsMode, Bool) -> Void,
toggleItem: @escaping (MessageReaction.Reaction) -> Void toggleItem: @escaping (MessageReaction.Reaction) -> Void
) { ) {
self.context = context self.context = context
@ -41,6 +41,7 @@ private enum PeerAllowedReactionListControllerSection: Int32 {
private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry { private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
enum StableId: Hashable { enum StableId: Hashable {
case allowSwitch
case allowAllHeader case allowAllHeader
case allowAll case allowAll
case allowSome case allowSome
@ -50,6 +51,7 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
case item(MessageReaction.Reaction) case item(MessageReaction.Reaction)
} }
case allowSwitch(text: String, value: Bool)
case allowAllHeader(String) case allowAllHeader(String)
case allowAll(text: String, isEnabled: Bool) case allowAll(text: String, isEnabled: Bool)
case allowSome(text: String, isEnabled: Bool) case allowSome(text: String, isEnabled: Bool)
@ -57,11 +59,11 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
case allowAllInfo(String) case allowAllInfo(String)
case itemsHeader(String) case itemsHeader(String)
case item(index: Int, value: MessageReaction.Reaction, availableReactions: AvailableReactions?, reaction: MessageReaction.Reaction, text: String, isEnabled: Bool) case item(index: Int, value: MessageReaction.Reaction, availableReactions: AvailableReactions?, reaction: MessageReaction.Reaction, text: String, isEnabled: Bool, allDisabled: Bool)
var section: ItemListSectionId { var section: ItemListSectionId {
switch self { switch self {
case .allowAllHeader, .allowAll, .allowSome, .allowNone, .allowAllInfo: case .allowSwitch, .allowAllHeader, .allowAll, .allowSome, .allowNone, .allowAllInfo:
return PeerAllowedReactionListControllerSection.all.rawValue return PeerAllowedReactionListControllerSection.all.rawValue
case .itemsHeader, .item: case .itemsHeader, .item:
return PeerAllowedReactionListControllerSection.items.rawValue return PeerAllowedReactionListControllerSection.items.rawValue
@ -70,6 +72,8 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
var stableId: StableId { var stableId: StableId {
switch self { switch self {
case .allowSwitch:
return .allowSwitch
case .allowAllHeader: case .allowAllHeader:
return .allowAllHeader return .allowAllHeader
case .allowAll: case .allowAll:
@ -82,32 +86,40 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
return .allowAllInfo return .allowAllInfo
case .itemsHeader: case .itemsHeader:
return .itemsHeader return .itemsHeader
case let .item(_, value, _, _, _, _): case let .item(_, value, _, _, _, _, _):
return .item(value) return .item(value)
} }
} }
var sortId: Int { var sortId: Int {
switch self { switch self {
case .allowAllHeader: case .allowSwitch:
return 0 return 0
case .allowAll: case .allowAllHeader:
return 1 return 1
case .allowSome: case .allowAll:
return 2 return 2
case .allowNone: case .allowSome:
return 3 return 3
case .allowAllInfo: case .allowNone:
return 4 return 4
case .itemsHeader: case .allowAllInfo:
return 5 return 5
case let .item(index, _, _, _, _, _): case .itemsHeader:
return 6
case let .item(index, _, _, _, _, _, _):
return 100 + index return 100 + index
} }
} }
static func ==(lhs: PeerAllowedReactionListControllerEntry, rhs: PeerAllowedReactionListControllerEntry) -> Bool { static func ==(lhs: PeerAllowedReactionListControllerEntry, rhs: PeerAllowedReactionListControllerEntry) -> Bool {
switch lhs { switch lhs {
case let .allowSwitch(text, value):
if case .allowSwitch(text, value) = rhs {
return true
} else {
return false
}
case let .allowAllHeader(text): case let .allowAllHeader(text):
if case .allowAllHeader(text) = rhs { if case .allowAllHeader(text) = rhs {
return true return true
@ -144,8 +156,8 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .item(index, value, availableReactions, reaction, text, isEnabled): case let .item(index, value, availableReactions, reaction, text, isEnabled, allDisabled):
if case .item(index, value, availableReactions, reaction, text, isEnabled) = rhs { if case .item(index, value, availableReactions, reaction, text, isEnabled, allDisabled) = rhs {
return true return true
} else { } else {
return false return false
@ -160,6 +172,14 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! PeerAllowedReactionListControllerArguments let arguments = arguments as! PeerAllowedReactionListControllerArguments
switch self { switch self {
case let .allowSwitch(text, value):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
if value {
arguments.setMode(.some, false)
} else {
arguments.setMode(.empty, false)
}
})
case let .allowAllHeader(text): case let .allowAllHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .allowAll(text, isEnabled): case let .allowAll(text, isEnabled):
@ -177,7 +197,7 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
zeroSeparatorInsets: false, zeroSeparatorInsets: false,
sectionId: self.section, sectionId: self.section,
action: { action: {
arguments.setMode(.all) arguments.setMode(.all, true)
}, },
deleteAction: nil deleteAction: nil
) )
@ -196,7 +216,7 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
zeroSeparatorInsets: false, zeroSeparatorInsets: false,
sectionId: self.section, sectionId: self.section,
action: { action: {
arguments.setMode(.some) arguments.setMode(.some, true)
}, },
deleteAction: nil deleteAction: nil
) )
@ -215,7 +235,7 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
zeroSeparatorInsets: false, zeroSeparatorInsets: false,
sectionId: self.section, sectionId: self.section,
action: { action: {
arguments.setMode(.empty) arguments.setMode(.empty, true)
}, },
deleteAction: nil deleteAction: nil
) )
@ -223,7 +243,7 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .itemsHeader(text): case let .itemsHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .item(_, value, availableReactions, reaction, text, isEnabled): case let .item(_, value, availableReactions, reaction, text, isEnabled, allDisabled):
return ItemListReactionItem( return ItemListReactionItem(
context: arguments.context, context: arguments.context,
presentationData: presentationData, presentationData: presentationData,
@ -231,6 +251,7 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
reaction: reaction, reaction: reaction,
title: text, title: text,
value: isEnabled, value: isEnabled,
enabled: !allDisabled,
sectionId: self.section, sectionId: self.section,
style: .blocks, style: .blocks,
updated: { _ in updated: { _ in
@ -255,7 +276,21 @@ private func peerAllowedReactionListControllerEntries(
) -> [PeerAllowedReactionListControllerEntry] { ) -> [PeerAllowedReactionListControllerEntry] {
var entries: [PeerAllowedReactionListControllerEntry] = [] var entries: [PeerAllowedReactionListControllerEntry] = []
if let availableReactions = availableReactions, let allowedReactions = state.updatedAllowedReactions, let mode = state.updatedMode { if let peer = peer, let availableReactions = availableReactions, let allowedReactions = state.updatedAllowedReactions, let mode = state.updatedMode {
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
//TODO:localize
entries.append(.allowSwitch(text: "Allow Reactions", value: mode != .empty))
entries.append(.itemsHeader(presentationData.strings.PeerInfo_AllowedReactions_ReactionListHeader))
var index = 0
for availableReaction in availableReactions.reactions {
if !availableReaction.isEnabled {
continue
}
entries.append(.item(index: index, value: availableReaction.value, availableReactions: availableReactions, reaction: availableReaction.value, text: availableReaction.title, isEnabled: allowedReactions.contains(availableReaction.value), allDisabled: mode == .empty))
index += 1
}
} else {
//TODO:localize //TODO:localize
entries.append(.allowAllHeader("AVAILABLE REACTIONS")) entries.append(.allowAllHeader("AVAILABLE REACTIONS"))
@ -279,7 +314,7 @@ private func peerAllowedReactionListControllerEntries(
case .all: case .all:
allInfoText = "Members of this group can use any emoji as reactions to messages." allInfoText = "Members of this group can use any emoji as reactions to messages."
case .some: case .some:
allInfoText = "You can select emoji that will allow members of your group to react to messages." allInfoText = "Members of the group can use only some allowed emoji as reactions to messages."
case .empty: case .empty:
allInfoText = "Members of the group can't add any reactions to messages." allInfoText = "Members of the group can't add any reactions to messages."
} }
@ -294,11 +329,12 @@ private func peerAllowedReactionListControllerEntries(
if !availableReaction.isEnabled { if !availableReaction.isEnabled {
continue continue
} }
entries.append(.item(index: index, value: availableReaction.value, availableReactions: availableReactions, reaction: availableReaction.value, text: availableReaction.title, isEnabled: allowedReactions.contains(availableReaction.value))) entries.append(.item(index: index, value: availableReaction.value, availableReactions: availableReactions, reaction: availableReaction.value, text: availableReaction.title, isEnabled: allowedReactions.contains(availableReaction.value), allDisabled: false))
index += 1 index += 1
} }
} }
} }
}
return entries return entries
} }
@ -330,7 +366,11 @@ public func peerAllowedReactionListController(
switch value { switch value {
case .all: case .all:
state.updatedMode = .all state.updatedMode = .all
if let availableReactions = availableReactions {
state.updatedAllowedReactions = Set(availableReactions.reactions.filter(\.isEnabled).map(\.value))
} else {
state.updatedAllowedReactions = Set() state.updatedAllowedReactions = Set()
}
case let .limited(reactions): case let .limited(reactions):
state.updatedMode = .some state.updatedMode = .some
state.updatedAllowedReactions = Set(reactions) state.updatedAllowedReactions = Set(reactions)
@ -346,7 +386,7 @@ public func peerAllowedReactionListController(
let arguments = PeerAllowedReactionListControllerArguments( let arguments = PeerAllowedReactionListControllerArguments(
context: context, context: context,
setMode: { mode in setMode: { mode, resetItems in
let _ = (context.engine.stickers.availableReactions() let _ = (context.engine.stickers.availableReactions()
|> take(1) |> take(1)
|> deliverOnMainQueue).start(next: { availableReactions in |> deliverOnMainQueue).start(next: { availableReactions in
@ -360,6 +400,7 @@ public func peerAllowedReactionListController(
if var updatedAllowedReactions = state.updatedAllowedReactions { if var updatedAllowedReactions = state.updatedAllowedReactions {
switch mode { switch mode {
case .all: case .all:
if resetItems {
updatedAllowedReactions.removeAll() updatedAllowedReactions.removeAll()
for availableReaction in availableReactions.reactions { for availableReaction in availableReactions.reactions {
if !availableReaction.isEnabled { if !availableReaction.isEnabled {
@ -367,7 +408,9 @@ public func peerAllowedReactionListController(
} }
updatedAllowedReactions.insert(availableReaction.value) updatedAllowedReactions.insert(availableReaction.value)
} }
}
case .some: case .some:
if resetItems {
updatedAllowedReactions.removeAll() updatedAllowedReactions.removeAll()
if let thumbsUp = availableReactions.reactions.first(where: { $0.value == .builtin("👍") }) { if let thumbsUp = availableReactions.reactions.first(where: { $0.value == .builtin("👍") }) {
updatedAllowedReactions.insert(thumbsUp.value) updatedAllowedReactions.insert(thumbsUp.value)
@ -375,8 +418,19 @@ public func peerAllowedReactionListController(
if let thumbsDown = availableReactions.reactions.first(where: { $0.value == .builtin("👎") }) { if let thumbsDown = availableReactions.reactions.first(where: { $0.value == .builtin("👎") }) {
updatedAllowedReactions.insert(thumbsDown.value) updatedAllowedReactions.insert(thumbsDown.value)
} }
case .empty: } else {
updatedAllowedReactions.removeAll() updatedAllowedReactions.removeAll()
for availableReaction in availableReactions.reactions {
if !availableReaction.isEnabled {
continue
}
updatedAllowedReactions.insert(availableReaction.value)
}
}
case .empty:
if resetItems {
updatedAllowedReactions.removeAll()
}
} }
state.updatedAllowedReactions = updatedAllowedReactions state.updatedAllowedReactions = updatedAllowedReactions
} }
@ -391,6 +445,9 @@ public func peerAllowedReactionListController(
if var updatedAllowedReactions = state.updatedAllowedReactions { if var updatedAllowedReactions = state.updatedAllowedReactions {
if updatedAllowedReactions.contains(reaction) { if updatedAllowedReactions.contains(reaction) {
updatedAllowedReactions.remove(reaction) updatedAllowedReactions.remove(reaction)
if state.updatedMode == .all {
state.updatedMode = .some
}
} else { } else {
updatedAllowedReactions.insert(reaction) updatedAllowedReactions.insert(reaction)
} }
@ -446,8 +503,20 @@ public func peerAllowedReactionListController(
let controller = ItemListController(context: context, state: signal) let controller = ItemListController(context: context, state: signal)
controller.willDisappear = { _ in controller.willDisappear = { _ in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.AllowedReactions(id: peerId)) let _ = (combineLatest(
|> deliverOnMainQueue).start(next: { initialAllowedReactions in context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
TelegramEngine.EngineData.Item.Peer.AllowedReactions(id: peerId)
),
context.engine.stickers.availableReactions() |> take(1)
)
|> deliverOnMainQueue).start(next: { data, availableReactions in
let (peer, initialAllowedReactions) = data
guard let peer = peer, let availableReactions = availableReactions else {
return
}
let state = stateValue.with({ $0 }) let state = stateValue.with({ $0 })
guard let updatedMode = state.updatedMode, let updatedAllowedReactions = state.updatedAllowedReactions else { guard let updatedMode = state.updatedMode, let updatedAllowedReactions = state.updatedAllowedReactions else {
return return
@ -458,7 +527,15 @@ public func peerAllowedReactionListController(
case .all: case .all:
updatedValue = .all updatedValue = .all
case .some: case .some:
if case let .channel(channel) = peer, case .broadcast = channel.info {
if updatedAllowedReactions == Set(availableReactions.reactions.filter(\.isEnabled).map(\.value)) {
updatedValue = .all
} else {
updatedValue = .limited(Array(updatedAllowedReactions)) updatedValue = .limited(Array(updatedAllowedReactions))
}
} else {
updatedValue = .limited(Array(updatedAllowedReactions))
}
case .empty: case .empty:
updatedValue = .empty updatedValue = .empty
} }

View File

@ -21,6 +21,7 @@ class EmojiHeaderComponent: Component {
let animationCache: AnimationCache let animationCache: AnimationCache
let animationRenderer: MultiAnimationRenderer let animationRenderer: MultiAnimationRenderer
let placeholderColor: UIColor let placeholderColor: UIColor
let accentColor: UIColor
let fileId: Int64 let fileId: Int64
let isVisible: Bool let isVisible: Bool
let hasIdleAnimations: Bool let hasIdleAnimations: Bool
@ -30,6 +31,7 @@ class EmojiHeaderComponent: Component {
animationCache: AnimationCache, animationCache: AnimationCache,
animationRenderer: MultiAnimationRenderer, animationRenderer: MultiAnimationRenderer,
placeholderColor: UIColor, placeholderColor: UIColor,
accentColor: UIColor,
fileId: Int64, fileId: Int64,
isVisible: Bool, isVisible: Bool,
hasIdleAnimations: Bool hasIdleAnimations: Bool
@ -38,13 +40,14 @@ class EmojiHeaderComponent: Component {
self.animationCache = animationCache self.animationCache = animationCache
self.animationRenderer = animationRenderer self.animationRenderer = animationRenderer
self.placeholderColor = placeholderColor self.placeholderColor = placeholderColor
self.accentColor = accentColor
self.fileId = fileId self.fileId = fileId
self.isVisible = isVisible self.isVisible = isVisible
self.hasIdleAnimations = hasIdleAnimations self.hasIdleAnimations = hasIdleAnimations
} }
static func ==(lhs: EmojiHeaderComponent, rhs: EmojiHeaderComponent) -> Bool { static func ==(lhs: EmojiHeaderComponent, rhs: EmojiHeaderComponent) -> Bool {
return lhs.placeholderColor == rhs.placeholderColor && lhs.fileId == rhs.fileId && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations return lhs.placeholderColor == rhs.placeholderColor && lhs.accentColor == rhs.accentColor && lhs.fileId == rhs.fileId && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations
} }
final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView { final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView {
@ -126,8 +129,11 @@ class EmojiHeaderComponent: Component {
content: .animation( content: .animation(
content: .customEmoji(fileId: component.fileId), content: .customEmoji(fileId: component.fileId),
size: CGSize(width: 100.0, height: 100.0), size: CGSize(width: 100.0, height: 100.0),
placeholderColor: component.placeholderColor placeholderColor: component.placeholderColor,
themeColor: component.accentColor,
loopMode: .forever
), ),
isVisibleForAnimations: true,
action: nil, action: nil,
longTapAction: nil longTapAction: nil
)), )),

View File

@ -1985,6 +1985,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
animationCache: state.animationCache, animationCache: state.animationCache,
animationRenderer: state.animationRenderer, animationRenderer: state.animationRenderer,
placeholderColor: environment.theme.list.mediaPlaceholderColor, placeholderColor: environment.theme.list.mediaPlaceholderColor,
accentColor: environment.theme.list.itemAccentColor,
fileId: fileId, fileId: fileId,
isVisible: starIsVisible, isVisible: starIsVisible,
hasIdleAnimations: state.hasIdleAnimations hasIdleAnimations: state.hasIdleAnimations

View File

@ -406,7 +406,7 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
targetContainerNode.view.superview?.bringSubviewToFront(targetContainerNode.view) targetContainerNode.view.superview?.bringSubviewToFront(targetContainerNode.view)
let standaloneReactionAnimation = StandaloneReactionAnimation(useDirectRendering: true) let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: nil, useDirectRendering: true)
self.standaloneReactionAnimation = standaloneReactionAnimation self.standaloneReactionAnimation = standaloneReactionAnimation
targetContainerNode.addSubnode(standaloneReactionAnimation) targetContainerNode.addSubnode(standaloneReactionAnimation)

View File

@ -32,6 +32,7 @@ swift_library(
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
"//submodules/TextFormat:TextFormat", "//submodules/TextFormat:TextFormat",
"//submodules/GZip:GZip",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -20,6 +20,7 @@ import AnimationCache
import MultiAnimationRenderer import MultiAnimationRenderer
import EmojiTextAttachmentView import EmojiTextAttachmentView
import TextFormat import TextFormat
import GZip
public final class ReactionItem { public final class ReactionItem {
public struct Reaction: Equatable { public struct Reaction: Equatable {
@ -106,7 +107,7 @@ private final class ExpandItemView: UIView {
} }
func updateTheme(theme: PresentationTheme) { func updateTheme(theme: PresentationTheme) {
self.backgroundColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor.mixedWith(theme.contextMenu.backgroundColor.withMultipliedAlpha(0.4), alpha: 0.5) self.backgroundColor = theme.chat.inputMediaPanel.panelContentControlVibrantOverlayColor.mixedWith(theme.contextMenu.backgroundColor.withMultipliedAlpha(0.4), alpha: 0.5)
} }
func update(size: CGSize, transition: ContainedViewLayoutTransition) { func update(size: CGSize, transition: ContainedViewLayoutTransition) {
@ -199,6 +200,45 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
private var hasPremium: Bool? private var hasPremium: Bool?
private var hasPremiumDisposable: Disposable? private var hasPremiumDisposable: Disposable?
private var genericReactionEffectDisposable: Disposable?
private var genericReactionEffect: String?
public static func randomGenericReactionEffect(context: AccountContext) -> Signal<String?, NoError> {
return context.engine.stickers.loadedStickerPack(reference: .emojiGenericAnimations, forceActualized: false)
|> map { result -> [TelegramMediaFile]? in
switch result {
case let .result(_, items, _):
return items.map(\.file)
default:
return nil
}
}
|> filter { $0 != nil }
|> take(1)
|> mapToSignal { items -> Signal<String?, NoError> in
guard let items = items else {
return .single(nil)
}
guard let file = items.randomElement() else {
return .single(nil)
}
return Signal { subscriber in
let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start()
let dataDisposable = (context.account.postbox.mediaBox.resourceData(file.resource)
|> filter(\.complete)
|> take(1)).start(next: { data in
subscriber.putNext(data.path)
subscriber.putCompletion()
})
return ActionDisposable {
fetchDisposable.dispose()
dataDisposable.dispose()
}
}
}
}
public init(context: AccountContext, animationCache: AnimationCache, presentationData: PresentationData, items: [ReactionContextItem], getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?, isExpandedUpdated: @escaping (ContainedViewLayoutTransition) -> Void, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void) { 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.context = context
self.presentationData = presentationData self.presentationData = presentationData
@ -351,12 +391,18 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
} }
}) })
} }
self.genericReactionEffectDisposable = (ReactionContextNode.randomGenericReactionEffect(context: context)
|> deliverOnMainQueue).start(next: { [weak self] path in
self?.genericReactionEffect = path
})
} }
deinit { deinit {
self.emojiContentDisposable?.dispose() self.emojiContentDisposable?.dispose()
self.availableReactionsDisposable?.dispose() self.availableReactionsDisposable?.dispose()
self.hasPremiumDisposable?.dispose() self.hasPremiumDisposable?.dispose()
self.genericReactionEffectDisposable?.dispose()
} }
override public func didLoad() { override public func didLoad() {
@ -702,7 +748,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
transition.updateFrame(node: self.leftBackgroundMaskNode, frame: CGRect(x: -1000.0 + currentMaskFrame.minX, y: 0.0, width: 1000.0, height: self.currentContentHeight + self.extensionDistance)) transition.updateFrame(node: self.leftBackgroundMaskNode, frame: CGRect(x: -1000.0 + currentMaskFrame.minX, y: 0.0, width: 1000.0, height: self.currentContentHeight + self.extensionDistance))
transition.updateFrame(node: self.rightBackgroundMaskNode, frame: CGRect(x: currentMaskFrame.maxX, y: 0.0, width: 1000.0, height: self.currentContentHeight + self.extensionDistance)) transition.updateFrame(node: self.rightBackgroundMaskNode, frame: CGRect(x: currentMaskFrame.maxX, y: 0.0, width: 1000.0, height: self.currentContentHeight + self.extensionDistance))
} else { } else {
self.leftBackgroundMaskNode.frame = CGRect(x: 0.0, y: 0.0, width: 1000.0, height: self.currentContentHeight + self.extensionDistance) transition.updateFrame(node: self.leftBackgroundMaskNode, frame: CGRect(x: 0.0, y: 0.0, width: 1000.0, height: self.currentContentHeight + self.extensionDistance))
self.rightBackgroundMaskNode.frame = CGRect(origin: .zero, size: .zero) self.rightBackgroundMaskNode.frame = CGRect(origin: .zero, size: .zero)
} }
@ -1175,16 +1221,22 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
return return
} }
//targetSnapshotView.layer.sublayers![0].backgroundColor = UIColor.green.cgColor
let sourceFrame = itemNode.view.convert(itemNode.bounds, to: self.view) let sourceFrame = itemNode.view.convert(itemNode.bounds, to: self.view)
var selfTargetBounds = targetView.bounds var selfTargetBounds = targetView.bounds
if case .builtin = itemNode.item.reaction.rawValue { if let targetView = targetView as? ReactionIconView, let iconFrame = targetView.iconFrame, !"".isEmpty {
selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5) selfTargetBounds = iconFrame
} }
/*if case .builtin = itemNode.item.reaction.rawValue {
selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5)
}*/
let targetFrame = self.view.convert(targetView.convert(selfTargetBounds, to: nil), from: nil) let targetFrame = self.view.convert(targetView.convert(selfTargetBounds, to: nil), from: nil)
targetSnapshotView.frame = targetFrame targetSnapshotView.frame = targetFrame
//targetSnapshotView.backgroundColor = .blue
self.view.insertSubview(targetSnapshotView, belowSubview: itemNode.view) self.view.insertSubview(targetSnapshotView, belowSubview: itemNode.view)
var completedTarget = false var completedTarget = false
@ -1199,6 +1251,14 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
let duration: Double = 0.16 let duration: Double = 0.16
itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.9, removeOnCompletion: false) itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.9, removeOnCompletion: false)
//itemNode.layer.isHidden = true
/*targetView.alpha = 1.0
targetView.isHidden = false
if let targetView = targetView as? ReactionIconView {
targetView.updateIsAnimationHidden(isAnimationHidden: false, transition: .immediate)
}*/
itemNode.layer.animatePosition(from: itemNode.layer.position, to: targetPosition, duration: duration, removeOnCompletion: false) itemNode.layer.animatePosition(from: itemNode.layer.position, to: targetPosition, duration: duration, removeOnCompletion: false)
targetSnapshotView.alpha = 1.0 targetSnapshotView.alpha = 1.0
targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.8) targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.8)
@ -1349,7 +1409,20 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
} else if itemNode.item.isCustom { } else if itemNode.item.isCustom {
additionalAnimationNode = nil additionalAnimationNode = nil
if let url = getAppBundle().url(forResource: self.didTriggerExpandedReaction ? "generic_reaction_effect" : "generic_reaction_small_effect", withExtension: "json"), let composition = Animation.filepath(url.path) { var effectData: Data?
if self.didTriggerExpandedReaction {
if let url = getAppBundle().url(forResource: "generic_reaction_effect", withExtension: "json") {
effectData = try? Data(contentsOf: url)
}
} else if let genericReactionEffect = self.genericReactionEffect, let data = try? Data(contentsOf: URL(fileURLWithPath: genericReactionEffect)) {
effectData = TGGUnzipData(data, 5 * 1024 * 1024) ?? data
} else {
if let url = getAppBundle().url(forResource: "generic_reaction_small_effect", withExtension: "json") {
effectData = try? Data(contentsOf: url)
}
}
if let effectData = effectData, let composition = try? Animation.from(data: effectData) {
let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable)) let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
view.animationSpeed = 1.0 view.animationSpeed = 1.0
view.backgroundColor = nil view.backgroundColor = nil
@ -1489,7 +1562,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
if self.didTriggerExpandedReaction { if self.didTriggerExpandedReaction {
self.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: hideNode, completion: { [weak self] in self.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: hideNode, completion: { [weak self] in
if let strongSelf = self, strongSelf.didTriggerExpandedReaction, let addStandaloneReactionAnimation = addStandaloneReactionAnimation { if let strongSelf = self, strongSelf.didTriggerExpandedReaction, let addStandaloneReactionAnimation = addStandaloneReactionAnimation {
let standaloneReactionAnimation = StandaloneReactionAnimation() let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: strongSelf.genericReactionEffect)
addStandaloneReactionAnimation(standaloneReactionAnimation) addStandaloneReactionAnimation(standaloneReactionAnimation)
@ -1775,6 +1848,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
} }
public final class StandaloneReactionAnimation: ASDisplayNode { public final class StandaloneReactionAnimation: ASDisplayNode {
private let genericReactionEffect: String?
private let useDirectRendering: Bool private let useDirectRendering: Bool
private var itemNode: ReactionNode? = nil private var itemNode: ReactionNode? = nil
private var itemNodeIsEmbedded: Bool = false private var itemNodeIsEmbedded: Bool = false
@ -1783,7 +1857,8 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
private weak var targetView: UIView? private weak var targetView: UIView?
public init(useDirectRendering: Bool = false) { public init(genericReactionEffect: String?, useDirectRendering: Bool = false) {
self.genericReactionEffect = genericReactionEffect
self.useDirectRendering = useDirectRendering self.useDirectRendering = useDirectRendering
super.init() super.init()
@ -1922,7 +1997,16 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
} else if itemNode.item.isCustom { } else if itemNode.item.isCustom {
additionalAnimationNode = nil additionalAnimationNode = nil
if let url = getAppBundle().url(forResource: "generic_reaction_small_effect", withExtension: "json"), let composition = Animation.filepath(url.path) { var effectData: Data?
if let genericReactionEffect = self.genericReactionEffect, let data = try? Data(contentsOf: URL(fileURLWithPath: genericReactionEffect)) {
effectData = TGGUnzipData(data, 5 * 1024 * 1024) ?? data
} else {
if let url = getAppBundle().url(forResource: "generic_reaction_small_effect", withExtension: "json") {
effectData = try? Data(contentsOf: url)
}
}
if let effectData = effectData, let composition = try? Animation.from(data: effectData) {
let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable)) let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
view.animationSpeed = 1.0 view.animationSpeed = 1.0
view.backgroundColor = nil view.backgroundColor = nil
@ -2043,9 +2127,10 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
intermediateCompletion() intermediateCompletion()
} else { } else {
if isLarge { if isLarge {
let genericReactionEffect = strongSelf.genericReactionEffect
strongSelf.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: true, completion: { strongSelf.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: true, completion: {
if let addStandaloneReactionAnimation = addStandaloneReactionAnimation { if let addStandaloneReactionAnimation = addStandaloneReactionAnimation {
let standaloneReactionAnimation = StandaloneReactionAnimation() let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: genericReactionEffect)
addStandaloneReactionAnimation(standaloneReactionAnimation) addStandaloneReactionAnimation(standaloneReactionAnimation)

View File

@ -291,7 +291,7 @@ public class ItemListReactionItemNode: ListViewItemNode, ItemListItemNode {
context: item.context, context: item.context,
animationCache: item.context.animationCache, animationCache: item.context.animationCache,
animationRenderer: item.context.animationRenderer, animationRenderer: item.context.animationRenderer,
content: .animation(content: animationContent, size: iconBoundingSize, placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor), action: nil, longTapAction: nil content: .animation(content: animationContent, size: iconBoundingSize, placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .forever), isVisibleForAnimations: true, action: nil, longTapAction: nil
)), )),
environment: {}, environment: {},
containerSize: iconBoundingSize containerSize: iconBoundingSize

View File

@ -92,6 +92,9 @@ class ReactionChatPreviewItemNode: ListViewItemNode {
private var animationCache: AnimationCache? private var animationCache: AnimationCache?
private var genericReactionEffect: String?
private var genericReactionEffectDisposable: Disposable?
init() { init() {
self.topStripeNode = ASDisplayNode() self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true self.topStripeNode.isLayerBacked = true
@ -111,6 +114,10 @@ class ReactionChatPreviewItemNode: ListViewItemNode {
self.addSubnode(self.containerNode) self.addSubnode(self.containerNode)
} }
deinit {
self.genericReactionEffectDisposable?.dispose()
}
override func didLoad() { override func didLoad() {
super.didLoad() super.didLoad()
@ -199,6 +206,16 @@ class ReactionChatPreviewItemNode: ListViewItemNode {
} }
} }
private func loadNextGenericReactionEffect(context: AccountContext) {
self.genericReactionEffectDisposable?.dispose()
self.genericReactionEffectDisposable = (ReactionContextNode.randomGenericReactionEffect(context: context) |> deliverOnMainQueue).start(next: { [weak self] path in
guard let strongSelf = self else {
return
}
strongSelf.genericReactionEffect = path
})
}
private func beginReactionAnimation(reactionItem: ReactionItem) { private func beginReactionAnimation(reactionItem: ReactionItem) {
if let item = self.item, let updatedReaction = item.reaction, let messageNode = self.messageNode as? ChatMessageItemNodeProtocol { if let item = self.item, let updatedReaction = item.reaction, let messageNode = self.messageNode as? ChatMessageItemNodeProtocol {
if let targetView = messageNode.targetReactionView(value: updatedReaction) { if let targetView = messageNode.targetReactionView(value: updatedReaction) {
@ -211,7 +228,8 @@ class ReactionChatPreviewItemNode: ListViewItemNode {
} }
if let supernode = self.supernode { if let supernode = self.supernode {
let standaloneReactionAnimation = StandaloneReactionAnimation() let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: self.genericReactionEffect)
self.loadNextGenericReactionEffect(context: item.context)
self.standaloneReactionAnimation = standaloneReactionAnimation self.standaloneReactionAnimation = standaloneReactionAnimation
let animationCache = item.context.animationCache let animationCache = item.context.animationCache
@ -309,6 +327,10 @@ class ReactionChatPreviewItemNode: ListViewItemNode {
strongSelf.item = item strongSelf.item = item
if strongSelf.genericReactionEffectDisposable == nil {
strongSelf.loadNextGenericReactionEffect(context: item.context)
}
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize) strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
var topOffset: CGFloat = 16.0 var topOffset: CGFloat = 16.0

View File

@ -362,7 +362,8 @@ final class StickerPackEmojisItemNode: GridItemNode {
content: .animation(animationData), content: .animation(animationData),
itemFile: item.file, itemFile: item.file,
subgroupId: nil, subgroupId: nil,
icon: .none icon: .none,
accentTint: false
), ),
context: context, context: context,
attemptSynchronousLoad: attemptSynchronousLoads, attemptSynchronousLoad: attemptSynchronousLoads,

View File

@ -354,6 +354,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[42402760] = { return Api.InputStickerSet.parse_inputStickerSetAnimatedEmoji($0) } dict[42402760] = { return Api.InputStickerSet.parse_inputStickerSetAnimatedEmoji($0) }
dict[215889721] = { return Api.InputStickerSet.parse_inputStickerSetAnimatedEmojiAnimations($0) } dict[215889721] = { return Api.InputStickerSet.parse_inputStickerSetAnimatedEmojiAnimations($0) }
dict[-427863538] = { return Api.InputStickerSet.parse_inputStickerSetDice($0) } dict[-427863538] = { return Api.InputStickerSet.parse_inputStickerSetDice($0) }
dict[701560302] = { return Api.InputStickerSet.parse_inputStickerSetEmojiDefaultStatuses($0) }
dict[80008398] = { return Api.InputStickerSet.parse_inputStickerSetEmojiGenericAnimations($0) }
dict[-4838507] = { return Api.InputStickerSet.parse_inputStickerSetEmpty($0) } dict[-4838507] = { return Api.InputStickerSet.parse_inputStickerSetEmpty($0) }
dict[-1645763991] = { return Api.InputStickerSet.parse_inputStickerSetID($0) } dict[-1645763991] = { return Api.InputStickerSet.parse_inputStickerSetID($0) }
dict[-930399486] = { return Api.InputStickerSet.parse_inputStickerSetPremiumGifts($0) } dict[-930399486] = { return Api.InputStickerSet.parse_inputStickerSetPremiumGifts($0) }

View File

@ -677,6 +677,8 @@ public extension Api {
case inputStickerSetAnimatedEmoji case inputStickerSetAnimatedEmoji
case inputStickerSetAnimatedEmojiAnimations case inputStickerSetAnimatedEmojiAnimations
case inputStickerSetDice(emoticon: String) case inputStickerSetDice(emoticon: String)
case inputStickerSetEmojiDefaultStatuses
case inputStickerSetEmojiGenericAnimations
case inputStickerSetEmpty case inputStickerSetEmpty
case inputStickerSetID(id: Int64, accessHash: Int64) case inputStickerSetID(id: Int64, accessHash: Int64)
case inputStickerSetPremiumGifts case inputStickerSetPremiumGifts
@ -701,6 +703,18 @@ public extension Api {
buffer.appendInt32(-427863538) buffer.appendInt32(-427863538)
} }
serializeString(emoticon, buffer: buffer, boxed: false) serializeString(emoticon, buffer: buffer, boxed: false)
break
case .inputStickerSetEmojiDefaultStatuses:
if boxed {
buffer.appendInt32(701560302)
}
break
case .inputStickerSetEmojiGenericAnimations:
if boxed {
buffer.appendInt32(80008398)
}
break break
case .inputStickerSetEmpty: case .inputStickerSetEmpty:
if boxed { if boxed {
@ -738,6 +752,10 @@ public extension Api {
return ("inputStickerSetAnimatedEmojiAnimations", []) return ("inputStickerSetAnimatedEmojiAnimations", [])
case .inputStickerSetDice(let emoticon): case .inputStickerSetDice(let emoticon):
return ("inputStickerSetDice", [("emoticon", String(describing: emoticon))]) return ("inputStickerSetDice", [("emoticon", String(describing: emoticon))])
case .inputStickerSetEmojiDefaultStatuses:
return ("inputStickerSetEmojiDefaultStatuses", [])
case .inputStickerSetEmojiGenericAnimations:
return ("inputStickerSetEmojiGenericAnimations", [])
case .inputStickerSetEmpty: case .inputStickerSetEmpty:
return ("inputStickerSetEmpty", []) return ("inputStickerSetEmpty", [])
case .inputStickerSetID(let id, let accessHash): case .inputStickerSetID(let id, let accessHash):
@ -766,6 +784,12 @@ public extension Api {
return nil return nil
} }
} }
public static func parse_inputStickerSetEmojiDefaultStatuses(_ reader: BufferReader) -> InputStickerSet? {
return Api.InputStickerSet.inputStickerSetEmojiDefaultStatuses
}
public static func parse_inputStickerSetEmojiGenericAnimations(_ reader: BufferReader) -> InputStickerSet? {
return Api.InputStickerSet.inputStickerSetEmojiGenericAnimations
}
public static func parse_inputStickerSetEmpty(_ reader: BufferReader) -> InputStickerSet? { public static func parse_inputStickerSetEmpty(_ reader: BufferReader) -> InputStickerSet? {
return Api.InputStickerSet.inputStickerSetEmpty return Api.InputStickerSet.inputStickerSetEmpty
} }

View File

@ -841,7 +841,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
} else if item.peer.isFake { } else if item.peer.isFake {
credibilityIcon = .fake(color: item.presentationData.theme.chat.message.incoming.scamColor) credibilityIcon = .fake(color: item.presentationData.theme.chat.message.incoming.scamColor)
} else if let user = item.peer as? TelegramUser, let emojiStatus = user.emojiStatus { } else if let user = item.peer as? TelegramUser, let emojiStatus = user.emojiStatus {
credibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor) credibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2))
} else if item.peer.isVerified { } else if item.peer.isVerified {
credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
} else if item.peer.isPremium && !premiumConfiguration.isPremiumDisabled { } else if item.peer.isPremium && !premiumConfiguration.isPremiumDisabled {
@ -1034,6 +1034,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
animationCache: animationCache, animationCache: animationCache,
animationRenderer: animationRenderer, animationRenderer: animationRenderer,
content: credibilityIcon, content: credibilityIcon,
isVisibleForAnimations: true,
action: nil, action: nil,
longTapAction: nil, longTapAction: nil,
emojiFileUpdated: nil emojiFileUpdated: nil

View File

@ -1171,7 +1171,8 @@ public class Account {
if !self.supplementary { if !self.supplementary {
self.managedOperationsDisposable.add(managedAnimatedEmojiUpdates(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedAnimatedEmojiUpdates(postbox: self.postbox, network: self.network).start())
self.managedOperationsDisposable.add(managedAnimatedEmojiAnimationsUpdates(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedAnimatedEmojiAnimationsUpdates(postbox: self.postbox, network: self.network).start())
} self.managedOperationsDisposable.add(managedGenericEmojiEffects(postbox: self.postbox, network: self.network).start())
self.managedOperationsDisposable.add(managedGreetingStickers(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedGreetingStickers(postbox: self.postbox, network: self.network).start())
self.managedOperationsDisposable.add(managedPremiumStickers(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedPremiumStickers(postbox: self.postbox, network: self.network).start())
self.managedOperationsDisposable.add(managedAllPremiumStickers(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedAllPremiumStickers(postbox: self.postbox, network: self.network).start())
@ -1179,6 +1180,7 @@ public class Account {
self.managedOperationsDisposable.add(managedFeaturedStatusEmoji(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedFeaturedStatusEmoji(postbox: self.postbox, network: self.network).start())
self.managedOperationsDisposable.add(managedRecentReactions(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedRecentReactions(postbox: self.postbox, network: self.network).start())
self.managedOperationsDisposable.add(managedTopReactions(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedTopReactions(postbox: self.postbox, network: self.network).start())
}
if !supplementary { if !supplementary {
let mediaBox = postbox.mediaBox let mediaBox = postbox.mediaBox

View File

@ -64,6 +64,10 @@ extension StickerPackReference {
self = .animatedEmojiAnimations self = .animatedEmojiAnimations
case .inputStickerSetPremiumGifts: case .inputStickerSetPremiumGifts:
self = .premiumGifts self = .premiumGifts
case .inputStickerSetEmojiGenericAnimations:
self = .emojiGenericAnimations
case .inputStickerSetEmojiDefaultStatuses:
return nil
} }
} }
} }

View File

@ -19,3 +19,11 @@ func managedAnimatedEmojiAnimationsUpdates(postbox: Postbox, network: Network) -
} }
return (poll |> then(.complete() |> suspendAwareDelay(2.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart return (poll |> then(.complete() |> suspendAwareDelay(2.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
} }
func managedGenericEmojiEffects(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = _internal_loadedStickerPack(postbox: postbox, network: network, reference: .emojiGenericAnimations, forceActualized: true)
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
return (poll |> then(.complete() |> suspendAwareDelay(2.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}

View File

@ -61,7 +61,7 @@ public func addSavedSticker(postbox: Postbox, network: Network, file: TelegramMe
if !found { if !found {
fetchReference = packReference fetchReference = packReference
} }
case .animatedEmoji, .animatedEmojiAnimations, .dice, .premiumGifts: case .animatedEmoji, .animatedEmojiAnimations, .dice, .premiumGifts, .emojiGenericAnimations:
break break
} }
if let fetchReference = fetchReference { if let fetchReference = fetchReference {

View File

@ -48,6 +48,7 @@ public struct Namespaces {
public static let CloudAnimatedEmojiReactions: Int32 = 6 public static let CloudAnimatedEmojiReactions: Int32 = 6
public static let CloudPremiumGifts: Int32 = 7 public static let CloudPremiumGifts: Int32 = 7
public static let CloudEmojiPacks: Int32 = 8 public static let CloudEmojiPacks: Int32 = 8
public static let CloudEmojiGenericAnimations: Int32 = 9
} }
public struct OrderedItemList { public struct OrderedItemList {

View File

@ -20,6 +20,7 @@ public enum StickerPackReference: PostboxCoding, Hashable, Equatable, Codable {
case dice(String) case dice(String)
case animatedEmojiAnimations case animatedEmojiAnimations
case premiumGifts case premiumGifts
case emojiGenericAnimations
public init(decoder: PostboxDecoder) { public init(decoder: PostboxDecoder) {
switch decoder.decodeInt32ForKey("r", orElse: 0) { switch decoder.decodeInt32ForKey("r", orElse: 0) {
@ -82,6 +83,8 @@ public enum StickerPackReference: PostboxCoding, Hashable, Equatable, Codable {
encoder.encodeInt32(4, forKey: "r") encoder.encodeInt32(4, forKey: "r")
case .premiumGifts: case .premiumGifts:
encoder.encodeInt32(5, forKey: "r") encoder.encodeInt32(5, forKey: "r")
case .emojiGenericAnimations:
preconditionFailure()
} }
} }
@ -105,6 +108,8 @@ public enum StickerPackReference: PostboxCoding, Hashable, Equatable, Codable {
try container.encode(4 as Int32, forKey: "r") try container.encode(4 as Int32, forKey: "r")
case .premiumGifts: case .premiumGifts:
try container.encode(5 as Int32, forKey: "r") try container.encode(5 as Int32, forKey: "r")
case .emojiGenericAnimations:
preconditionFailure()
} }
} }
@ -146,6 +151,12 @@ public enum StickerPackReference: PostboxCoding, Hashable, Equatable, Codable {
} else { } else {
return false return false
} }
case .emojiGenericAnimations:
if case .emojiGenericAnimations = rhs {
return true
} else {
return false
}
} }
} }
} }

View File

@ -138,6 +138,20 @@ func _internal_cachedStickerPack(postbox: Postbox, network: Network, reference:
} else { } else {
return (.fetching, true, nil) return (.fetching, true, nil)
} }
case .emojiGenericAnimations:
let namespace = Namespaces.ItemCollection.CloudEmojiGenericAnimations
let id: ItemCollectionId.Id = 0
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
previousHash = cached.hash
let current: CachedStickerPackResult = .result(info, cached.items, false)
if cached.hash != info.hash {
return (current, true, previousHash)
} else {
return (current, false, previousHash)
}
} else {
return (.fetching, true, nil)
}
} }
} }
|> mapToSignal { result, loadRemote, previousHash in |> mapToSignal { result, loadRemote, previousHash in
@ -246,6 +260,18 @@ func cachedStickerPack(transaction: Transaction, reference: StickerPackReference
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info { if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
return (info, cached.items, false) return (info, cached.items, false)
} }
case .emojiGenericAnimations:
let namespace = Namespaces.ItemCollection.CloudEmojiGenericAnimations
let id: ItemCollectionId.Id = 0
if let currentInfo = transaction.getItemCollectionInfo(collectionId: ItemCollectionId(namespace: namespace, id: id)) as? StickerPackCollectionInfo {
let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: namespace, id: id))
if !items.isEmpty {
return (currentInfo, items.compactMap { $0 as? StickerPackItem }, true)
}
}
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
return (info, cached.items, false)
}
} }
return nil return nil
} }

View File

@ -22,6 +22,8 @@ extension StickerPackReference {
return .inputStickerSetAnimatedEmojiAnimations return .inputStickerSetAnimatedEmojiAnimations
case .premiumGifts: case .premiumGifts:
return .inputStickerSetPremiumGifts return .inputStickerSetPremiumGifts
case .emojiGenericAnimations:
return .inputStickerSetEmojiGenericAnimations
} }
} }
} }

View File

@ -46,6 +46,9 @@ func _internal_requestStickerSet(postbox: Postbox, network: Network, reference:
case .premiumGifts: case .premiumGifts:
collectionId = nil collectionId = nil
input = .inputStickerSetPremiumGifts input = .inputStickerSetPremiumGifts
case .emojiGenericAnimations:
collectionId = nil
input = .inputStickerSetEmojiGenericAnimations
} }
let localSignal: (ItemCollectionId) -> Signal<(ItemCollectionInfo, [ItemCollectionItem])?, NoError> = { collectionId in let localSignal: (ItemCollectionId) -> Signal<(ItemCollectionInfo, [ItemCollectionItem])?, NoError> = { collectionId in

View File

@ -610,6 +610,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati
panelHighlightedIconBackgroundColor: UIColor(rgb: 0x808080).withMultipliedAlpha(0.25), panelHighlightedIconBackgroundColor: UIColor(rgb: 0x808080).withMultipliedAlpha(0.25),
panelHighlightedIconColor: UIColor(rgb: 0x808080).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.35), panelHighlightedIconColor: UIColor(rgb: 0x808080).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.35),
panelContentVibrantOverlayColor: UIColor(rgb: 0x808080), panelContentVibrantOverlayColor: UIColor(rgb: 0x808080),
panelContentControlVibrantOverlayColor: UIColor(rgb: 0x808080).mixedWith(UIColor(rgb: 0x000000), alpha: 0.35),
stickersBackgroundColor: UIColor(rgb: 0x000000), stickersBackgroundColor: UIColor(rgb: 0x000000),
stickersSectionTextColor: UIColor(rgb: 0x7b7b7b), stickersSectionTextColor: UIColor(rgb: 0x7b7b7b),
stickersSearchBackgroundColor: UIColor(rgb: 0x1c1c1d), stickersSearchBackgroundColor: UIColor(rgb: 0x1c1c1d),

View File

@ -442,6 +442,7 @@ public func customizeDefaultDarkTintedPresentationTheme(theme: PresentationTheme
panelHighlightedIconBackgroundColor: mainSecondaryTextColor?.withAlphaComponent(0.5).withMultipliedAlpha(0.25), panelHighlightedIconBackgroundColor: mainSecondaryTextColor?.withAlphaComponent(0.5).withMultipliedAlpha(0.25),
panelHighlightedIconColor: mainSecondaryTextColor?.withAlphaComponent(0.5).mixedWith(chat.inputPanel.primaryTextColor, alpha: 0.35), panelHighlightedIconColor: mainSecondaryTextColor?.withAlphaComponent(0.5).mixedWith(chat.inputPanel.primaryTextColor, alpha: 0.35),
panelContentVibrantOverlayColor: mainSecondaryTextColor?.withAlphaComponent(0.5), panelContentVibrantOverlayColor: mainSecondaryTextColor?.withAlphaComponent(0.5),
panelContentControlVibrantOverlayColor: mainSecondaryTextColor?.withAlphaComponent(0.3),
stickersBackgroundColor: additionalBackgroundColor, stickersBackgroundColor: additionalBackgroundColor,
stickersSectionTextColor: mainSecondaryTextColor?.withAlphaComponent(0.5), stickersSectionTextColor: mainSecondaryTextColor?.withAlphaComponent(0.5),
stickersSearchBackgroundColor: accentColor?.withMultiplied(hue: 1.009, saturation: 0.621, brightness: 0.15), stickersSearchBackgroundColor: accentColor?.withMultiplied(hue: 1.009, saturation: 0.621, brightness: 0.15),
@ -842,6 +843,7 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres
panelHighlightedIconBackgroundColor: mainSecondaryTextColor.withAlphaComponent(0.5).withMultipliedAlpha(0.25), panelHighlightedIconBackgroundColor: mainSecondaryTextColor.withAlphaComponent(0.5).withMultipliedAlpha(0.25),
panelHighlightedIconColor: mainSecondaryTextColor.withAlphaComponent(0.5).mixedWith(inputPanel.primaryTextColor, alpha: 0.35), panelHighlightedIconColor: mainSecondaryTextColor.withAlphaComponent(0.5).mixedWith(inputPanel.primaryTextColor, alpha: 0.35),
panelContentVibrantOverlayColor: mainSecondaryTextColor.withAlphaComponent(0.5), panelContentVibrantOverlayColor: mainSecondaryTextColor.withAlphaComponent(0.5),
panelContentControlVibrantOverlayColor: mainSecondaryTextColor.withAlphaComponent(0.3),
stickersBackgroundColor: additionalBackgroundColor, stickersBackgroundColor: additionalBackgroundColor,
stickersSectionTextColor: mainSecondaryTextColor.withAlphaComponent(0.5), stickersSectionTextColor: mainSecondaryTextColor.withAlphaComponent(0.5),
stickersSearchBackgroundColor: accentColor.withMultiplied(hue: 1.009, saturation: 0.621, brightness: 0.15), stickersSearchBackgroundColor: accentColor.withMultiplied(hue: 1.009, saturation: 0.621, brightness: 0.15),

View File

@ -863,7 +863,8 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
panelIconColor: UIColor(rgb: 0x858e99), panelIconColor: UIColor(rgb: 0x858e99),
panelHighlightedIconBackgroundColor: UIColor(rgb: 0x858e99, alpha: 0.2), panelHighlightedIconBackgroundColor: UIColor(rgb: 0x858e99, alpha: 0.2),
panelHighlightedIconColor: UIColor(rgb: 0x4D5561), panelHighlightedIconColor: UIColor(rgb: 0x4D5561),
panelContentVibrantOverlayColor: day ? UIColor(white: 0.0, alpha: 0.3) : UIColor(white: 0.7, alpha: 0.65), panelContentVibrantOverlayColor: UIColor(white: 0.7, alpha: 0.65),
panelContentControlVibrantOverlayColor: UIColor(white: 0.85, alpha: 0.65),
stickersBackgroundColor: UIColor(rgb: 0xe8ebf0), stickersBackgroundColor: UIColor(rgb: 0xe8ebf0),
stickersSectionTextColor: UIColor(rgb: 0x9099a2), stickersSectionTextColor: UIColor(rgb: 0x9099a2),
stickersSearchBackgroundColor: UIColor(rgb: 0xd9dbe1), stickersSearchBackgroundColor: UIColor(rgb: 0xd9dbe1),

View File

@ -1144,6 +1144,7 @@ public final class PresentationThemeInputMediaPanel {
public let panelHighlightedIconBackgroundColor: UIColor public let panelHighlightedIconBackgroundColor: UIColor
public let panelHighlightedIconColor: UIColor public let panelHighlightedIconColor: UIColor
public let panelContentVibrantOverlayColor: UIColor public let panelContentVibrantOverlayColor: UIColor
public let panelContentControlVibrantOverlayColor: UIColor
public let stickersBackgroundColor: UIColor public let stickersBackgroundColor: UIColor
public let stickersSectionTextColor: UIColor public let stickersSectionTextColor: UIColor
public let stickersSearchBackgroundColor: UIColor public let stickersSearchBackgroundColor: UIColor
@ -1153,12 +1154,28 @@ public final class PresentationThemeInputMediaPanel {
public let gifsBackgroundColor: UIColor public let gifsBackgroundColor: UIColor
public let backgroundColor: UIColor public let backgroundColor: UIColor
public init(panelSeparatorColor: UIColor, panelIconColor: UIColor, panelHighlightedIconBackgroundColor: UIColor, panelHighlightedIconColor: UIColor, panelContentVibrantOverlayColor: UIColor, stickersBackgroundColor: UIColor, stickersSectionTextColor: UIColor, stickersSearchBackgroundColor: UIColor, stickersSearchPlaceholderColor: UIColor, stickersSearchPrimaryColor: UIColor, stickersSearchControlColor: UIColor, gifsBackgroundColor: UIColor, backgroundColor: UIColor) { public init(
panelSeparatorColor: UIColor,
panelIconColor: UIColor,
panelHighlightedIconBackgroundColor: UIColor,
panelHighlightedIconColor: UIColor,
panelContentVibrantOverlayColor: UIColor,
panelContentControlVibrantOverlayColor: UIColor,
stickersBackgroundColor: UIColor,
stickersSectionTextColor: UIColor,
stickersSearchBackgroundColor: UIColor,
stickersSearchPlaceholderColor: UIColor,
stickersSearchPrimaryColor: UIColor,
stickersSearchControlColor: UIColor,
gifsBackgroundColor: UIColor,
backgroundColor: UIColor
) {
self.panelSeparatorColor = panelSeparatorColor self.panelSeparatorColor = panelSeparatorColor
self.panelIconColor = panelIconColor self.panelIconColor = panelIconColor
self.panelHighlightedIconBackgroundColor = panelHighlightedIconBackgroundColor self.panelHighlightedIconBackgroundColor = panelHighlightedIconBackgroundColor
self.panelHighlightedIconColor = panelHighlightedIconColor self.panelHighlightedIconColor = panelHighlightedIconColor
self.panelContentVibrantOverlayColor = panelContentVibrantOverlayColor self.panelContentVibrantOverlayColor = panelContentVibrantOverlayColor
self.panelContentControlVibrantOverlayColor = panelContentControlVibrantOverlayColor
self.stickersBackgroundColor = stickersBackgroundColor self.stickersBackgroundColor = stickersBackgroundColor
self.stickersSectionTextColor = stickersSectionTextColor self.stickersSectionTextColor = stickersSectionTextColor
self.stickersSearchBackgroundColor = stickersSearchBackgroundColor self.stickersSearchBackgroundColor = stickersSearchBackgroundColor
@ -1169,8 +1186,37 @@ public final class PresentationThemeInputMediaPanel {
self.backgroundColor = backgroundColor self.backgroundColor = backgroundColor
} }
public func withUpdated(panelSeparatorColor: UIColor? = nil, panelIconColor: UIColor? = nil, panelHighlightedIconBackgroundColor: UIColor? = nil, panelHighlightedIconColor: UIColor? = nil, panelContentVibrantOverlayColor: UIColor? = nil, stickersBackgroundColor: UIColor? = nil, stickersSectionTextColor: UIColor? = nil, stickersSearchBackgroundColor: UIColor? = nil, stickersSearchPlaceholderColor: UIColor? = nil, stickersSearchPrimaryColor: UIColor? = nil, stickersSearchControlColor: UIColor? = nil, gifsBackgroundColor: UIColor? = nil, backgroundColor: UIColor? = nil) -> PresentationThemeInputMediaPanel { public func withUpdated(
return PresentationThemeInputMediaPanel(panelSeparatorColor: panelSeparatorColor ?? self.panelSeparatorColor, panelIconColor: panelIconColor ?? self.panelIconColor, panelHighlightedIconBackgroundColor: panelHighlightedIconBackgroundColor ?? self.panelHighlightedIconBackgroundColor, panelHighlightedIconColor: panelHighlightedIconColor ?? self.panelHighlightedIconColor, panelContentVibrantOverlayColor: panelContentVibrantOverlayColor ?? self.panelContentVibrantOverlayColor, stickersBackgroundColor: stickersBackgroundColor ?? self.stickersBackgroundColor, stickersSectionTextColor: stickersSectionTextColor ?? self.stickersSectionTextColor, stickersSearchBackgroundColor: stickersSearchBackgroundColor ?? self.stickersSearchBackgroundColor, stickersSearchPlaceholderColor: stickersSearchPlaceholderColor ?? self.stickersSearchPlaceholderColor, stickersSearchPrimaryColor: stickersSearchPrimaryColor ?? self.stickersSearchPrimaryColor, stickersSearchControlColor: stickersSearchControlColor ?? self.stickersSearchControlColor, gifsBackgroundColor: gifsBackgroundColor ?? self.gifsBackgroundColor, backgroundColor: backgroundColor ?? self.backgroundColor) panelSeparatorColor: UIColor? = nil,
panelIconColor: UIColor? = nil,
panelHighlightedIconBackgroundColor: UIColor? = nil,
panelHighlightedIconColor: UIColor? = nil,
panelContentVibrantOverlayColor: UIColor? = nil,
panelContentControlVibrantOverlayColor: UIColor? = nil,
stickersBackgroundColor: UIColor? = nil,
stickersSectionTextColor: UIColor? = nil,
stickersSearchBackgroundColor: UIColor? = nil,
stickersSearchPlaceholderColor: UIColor? = nil,
stickersSearchPrimaryColor: UIColor? = nil,
stickersSearchControlColor: UIColor? = nil,
gifsBackgroundColor: UIColor? = nil,
backgroundColor: UIColor? = nil
) -> PresentationThemeInputMediaPanel {
return PresentationThemeInputMediaPanel(
panelSeparatorColor: panelSeparatorColor ?? self.panelSeparatorColor,
panelIconColor: panelIconColor ?? self.panelIconColor,
panelHighlightedIconBackgroundColor: panelHighlightedIconBackgroundColor ?? self.panelHighlightedIconBackgroundColor,
panelHighlightedIconColor: panelHighlightedIconColor ?? self.panelHighlightedIconColor,
panelContentVibrantOverlayColor: panelContentVibrantOverlayColor ?? self.panelContentVibrantOverlayColor,
panelContentControlVibrantOverlayColor: panelContentControlVibrantOverlayColor ?? self.panelContentControlVibrantOverlayColor,
stickersBackgroundColor: stickersBackgroundColor ?? self.stickersBackgroundColor,
stickersSectionTextColor: stickersSectionTextColor ?? self.stickersSectionTextColor,
stickersSearchBackgroundColor: stickersSearchBackgroundColor ?? self.stickersSearchBackgroundColor,
stickersSearchPlaceholderColor: stickersSearchPlaceholderColor ?? self.stickersSearchPlaceholderColor,
stickersSearchPrimaryColor: stickersSearchPrimaryColor ?? self.stickersSearchPrimaryColor, stickersSearchControlColor: stickersSearchControlColor ?? self.stickersSearchControlColor,
gifsBackgroundColor: gifsBackgroundColor ?? self.gifsBackgroundColor,
backgroundColor: backgroundColor ?? self.backgroundColor
)
} }
} }

View File

@ -1607,6 +1607,7 @@ extension PresentationThemeInputMediaPanel: Codable {
case panelHighlightedIconBg case panelHighlightedIconBg
case panelHighlightedIcon case panelHighlightedIcon
case panelContentVibrantOverlay case panelContentVibrantOverlay
case panelContentControlVibrantOverlay
case stickersBg case stickersBg
case stickersSectionText case stickersSectionText
case stickersSearchBg case stickersSearchBg
@ -1644,6 +1645,7 @@ extension PresentationThemeInputMediaPanel: Codable {
panelHighlightedIconBackgroundColor: try decodeColor(values, .panelHighlightedIconBg), panelHighlightedIconBackgroundColor: try decodeColor(values, .panelHighlightedIconBg),
panelHighlightedIconColor: panelHighlightedIconColor, panelHighlightedIconColor: panelHighlightedIconColor,
panelContentVibrantOverlayColor: try decodeColor(values, .panelContentVibrantOverlay, fallbackKey: "\(codingPath).stickersSectionText"), panelContentVibrantOverlayColor: try decodeColor(values, .panelContentVibrantOverlay, fallbackKey: "\(codingPath).stickersSectionText"),
panelContentControlVibrantOverlayColor: try decodeColor(values, .panelContentControlVibrantOverlay, fallbackKey: "\(codingPath).stickersSectionText"),
stickersBackgroundColor: try decodeColor(values, .stickersBg), stickersBackgroundColor: try decodeColor(values, .stickersBg),
stickersSectionTextColor: try decodeColor(values, .stickersSectionText), stickersSectionTextColor: try decodeColor(values, .stickersSectionText),
stickersSearchBackgroundColor: try decodeColor(values, .stickersSearchBg), stickersSearchBackgroundColor: try decodeColor(values, .stickersSearchBg),
@ -1660,6 +1662,7 @@ extension PresentationThemeInputMediaPanel: Codable {
try encodeColor(&values, self.panelHighlightedIconBackgroundColor, .panelHighlightedIconBg) try encodeColor(&values, self.panelHighlightedIconBackgroundColor, .panelHighlightedIconBg)
try encodeColor(&values, self.panelHighlightedIconColor, .panelHighlightedIcon) try encodeColor(&values, self.panelHighlightedIconColor, .panelHighlightedIcon)
try encodeColor(&values, self.panelContentVibrantOverlayColor, .panelContentVibrantOverlay) try encodeColor(&values, self.panelContentVibrantOverlayColor, .panelContentVibrantOverlay)
try encodeColor(&values, self.panelContentControlVibrantOverlayColor, .panelContentControlVibrantOverlay)
try encodeColor(&values, self.stickersBackgroundColor, .stickersBg) try encodeColor(&values, self.stickersBackgroundColor, .stickersBg)
try encodeColor(&values, self.stickersSectionTextColor, .stickersSectionText) try encodeColor(&values, self.stickersSectionTextColor, .stickersSectionText)
try encodeColor(&values, self.stickersSearchBackgroundColor, .stickersSearchBg) try encodeColor(&values, self.stickersSearchBackgroundColor, .stickersSearchBg)

View File

@ -54,17 +54,27 @@ public final class AnimationCacheItem {
case frames(Int) case frames(Int)
} }
public struct AdvanceResult {
public let frame: AnimationCacheItemFrame
public let didLoop: Bool
public init(frame: AnimationCacheItemFrame, didLoop: Bool) {
self.frame = frame
self.didLoop = didLoop
}
}
public let numFrames: Int public let numFrames: Int
private let advanceImpl: (Advance, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame? private let advanceImpl: (Advance, AnimationCacheItemFrame.RequestedFormat) -> AdvanceResult?
private let resetImpl: () -> Void private let resetImpl: () -> Void
public init(numFrames: Int, advanceImpl: @escaping (Advance, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame?, resetImpl: @escaping () -> Void) { public init(numFrames: Int, advanceImpl: @escaping (Advance, AnimationCacheItemFrame.RequestedFormat) -> AdvanceResult?, resetImpl: @escaping () -> Void) {
self.numFrames = numFrames self.numFrames = numFrames
self.advanceImpl = advanceImpl self.advanceImpl = advanceImpl
self.resetImpl = resetImpl self.resetImpl = resetImpl
} }
public func advance(advance: Advance, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame? { public func advance(advance: Advance, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AdvanceResult? {
return self.advanceImpl(advance, requestedFormat) return self.advanceImpl(advance, requestedFormat)
} }
@ -669,12 +679,14 @@ private final class AnimationCacheItemAccessor {
self.currentDctData = dctData self.currentDctData = dctData
} }
private func loadNextFrame() { private func loadNextFrame() -> Bool {
var didLoop = false
let index: Int let index: Int
if let currentFrame = self.currentFrame { if let currentFrame = self.currentFrame {
if currentFrame.index + 1 >= self.durationMapping.count { if currentFrame.index + 1 >= self.durationMapping.count {
index = 0 index = 0
self.compressedDataReader = nil self.compressedDataReader = nil
didLoop = true
} else { } else {
index = currentFrame.index + 1 index = currentFrame.index + 1
} }
@ -689,7 +701,7 @@ private final class AnimationCacheItemAccessor {
guard let compressedDataReader = self.compressedDataReader else { guard let compressedDataReader = self.compressedDataReader else {
self.currentFrame = nil self.currentFrame = nil
return return didLoop
} }
do { do {
@ -779,17 +791,22 @@ private final class AnimationCacheItemAccessor {
self.currentFrame = nil self.currentFrame = nil
self.compressedDataReader = nil self.compressedDataReader = nil
} }
return didLoop
} }
func reset() { func reset() {
self.currentFrame = nil self.currentFrame = nil
} }
func advance(advance: AnimationCacheItem.Advance, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame? { func advance(advance: AnimationCacheItem.Advance, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItem.AdvanceResult? {
var didLoop = false
switch advance { switch advance {
case let .frames(count): case let .frames(count):
for _ in 0 ..< count { for _ in 0 ..< count {
self.loadNextFrame() if self.loadNextFrame() {
didLoop = true
}
} }
case let .duration(duration): case let .duration(duration):
var durationOverflow = duration var durationOverflow = duration
@ -798,12 +815,16 @@ private final class AnimationCacheItemAccessor {
currentFrame.remainingDuration -= durationOverflow currentFrame.remainingDuration -= durationOverflow
if currentFrame.remainingDuration <= 0.0 { if currentFrame.remainingDuration <= 0.0 {
durationOverflow = -currentFrame.remainingDuration durationOverflow = -currentFrame.remainingDuration
self.loadNextFrame() if self.loadNextFrame() {
didLoop = true
}
} else { } else {
break break
} }
} else { } else {
self.loadNextFrame() if self.loadNextFrame() {
didLoop = true
}
break break
} }
} }
@ -818,9 +839,13 @@ private final class AnimationCacheItemAccessor {
let currentSurface = ImageARGB(width: currentFrame.yuva.yPlane.width, height: currentFrame.yuva.yPlane.height, rowAlignment: 32) let currentSurface = ImageARGB(width: currentFrame.yuva.yPlane.width, height: currentFrame.yuva.yPlane.height, rowAlignment: 32)
currentFrame.yuva.toARGB(target: currentSurface) currentFrame.yuva.toARGB(target: currentSurface)
return AnimationCacheItemFrame(format: .rgba(data: currentSurface.argbPlane.data, width: currentSurface.argbPlane.width, height: currentSurface.argbPlane.height, bytesPerRow: currentSurface.argbPlane.bytesPerRow), duration: currentFrame.duration) return AnimationCacheItem.AdvanceResult(
frame: AnimationCacheItemFrame(format: .rgba(data: currentSurface.argbPlane.data, width: currentSurface.argbPlane.width, height: currentSurface.argbPlane.height, bytesPerRow: currentSurface.argbPlane.bytesPerRow), duration: currentFrame.duration),
didLoop: didLoop
)
case .yuva: case .yuva:
return AnimationCacheItemFrame( return AnimationCacheItem.AdvanceResult(
frame: AnimationCacheItemFrame(
format: .yuva( format: .yuva(
y: AnimationCacheItemFrame.Plane( y: AnimationCacheItemFrame.Plane(
data: currentFrame.yuva.yPlane.data, data: currentFrame.yuva.yPlane.data,
@ -848,6 +873,8 @@ private final class AnimationCacheItemAccessor {
) )
), ),
duration: currentFrame.duration duration: currentFrame.duration
),
didLoop: didLoop
) )
} }
} }
@ -1235,7 +1262,7 @@ private func adaptItemFromHigherResolution(currentQueue: Queue, itemPath: String
guard let frame = higherResolutionItem.advance(advance: .frames(1), requestedFormat: .yuva(rowAlignment: yuva.yPlane.rowAlignment)) else { guard let frame = higherResolutionItem.advance(advance: .frames(1), requestedFormat: .yuva(rowAlignment: yuva.yPlane.rowAlignment)) else {
return nil return nil
} }
switch frame.format { switch frame.frame.format {
case .rgba: case .rgba:
return nil return nil
case let .yuva(y, u, v, a): case let .yuva(y, u, v, a):
@ -1245,7 +1272,7 @@ private func adaptItemFromHigherResolution(currentQueue: Queue, itemPath: String
yuva.aPlane.copyScaled(fromPlane: a) yuva.aPlane.copyScaled(fromPlane: a)
} }
return frame.duration return frame.frame.duration
}, proposedWidth: width, proposedHeight: height, insertKeyframe: true) }, proposedWidth: width, proposedHeight: height, insertKeyframe: true)
} }
@ -1282,7 +1309,7 @@ private func generateFirstFrameFromItem(currentQueue: Queue, itemPath: String, a
guard let frame = animationItem.advance(advance: .frames(1), requestedFormat: .yuva(rowAlignment: 1)) else { guard let frame = animationItem.advance(advance: .frames(1), requestedFormat: .yuva(rowAlignment: 1)) else {
return false return false
} }
switch frame.format { switch frame.frame.format {
case .rgba: case .rgba:
return false return false
case let .yuva(y, u, v, a): case let .yuva(y, u, v, a):
@ -1297,7 +1324,7 @@ private func generateFirstFrameFromItem(currentQueue: Queue, itemPath: String, a
yuva.vPlane.copyScaled(fromPlane: v) yuva.vPlane.copyScaled(fromPlane: v)
yuva.aPlane.copyScaled(fromPlane: a) yuva.aPlane.copyScaled(fromPlane: a)
return frame.duration return frame.frame.duration
}, proposedWidth: y.width, proposedHeight: y.height, insertKeyframe: true) }, proposedWidth: y.width, proposedHeight: y.height, insertKeyframe: true)
} }
} }

View File

@ -13,6 +13,7 @@ swift_library(
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Display:Display", "//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow", "//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
@ -21,6 +22,8 @@ swift_library(
"//submodules/TelegramCore:TelegramCore", "//submodules/TelegramCore:TelegramCore",
"//submodules/AppBundle:AppBundle", "//submodules/AppBundle:AppBundle",
"//submodules/TextFormat:TextFormat", "//submodules/TextFormat:TextFormat",
"//submodules/lottie-ios:Lottie",
"//submodules/GZip:GZip",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -11,6 +11,9 @@ import Postbox
import EmojiTextAttachmentView import EmojiTextAttachmentView
import AppBundle import AppBundle
import TextFormat import TextFormat
import Lottie
import GZip
import HierarchyTrackingLayer
public final class EmojiStatusComponent: Component { public final class EmojiStatusComponent: Component {
public typealias EnvironmentType = Empty public typealias EnvironmentType = Empty
@ -29,19 +32,25 @@ public final class EmojiStatusComponent: Component {
} }
} }
public enum LoopMode: Equatable {
case forever
case count(Int)
}
public enum Content: Equatable { public enum Content: Equatable {
case none case none
case premium(color: UIColor) case premium(color: UIColor)
case verified(fillColor: UIColor, foregroundColor: UIColor) case verified(fillColor: UIColor, foregroundColor: UIColor)
case fake(color: UIColor) case fake(color: UIColor)
case scam(color: UIColor) case scam(color: UIColor)
case animation(content: AnimationContent, size: CGSize, placeholderColor: UIColor) case animation(content: AnimationContent, size: CGSize, placeholderColor: UIColor, themeColor: UIColor?, loopMode: LoopMode)
} }
public let context: AccountContext public let context: AccountContext
public let animationCache: AnimationCache public let animationCache: AnimationCache
public let animationRenderer: MultiAnimationRenderer public let animationRenderer: MultiAnimationRenderer
public let content: Content public let content: Content
public let isVisibleForAnimations: Bool
public let action: (() -> Void)? public let action: (() -> Void)?
public let longTapAction: (() -> Void)? public let longTapAction: (() -> Void)?
public let emojiFileUpdated: ((TelegramMediaFile?) -> Void)? public let emojiFileUpdated: ((TelegramMediaFile?) -> Void)?
@ -51,6 +60,7 @@ public final class EmojiStatusComponent: Component {
animationCache: AnimationCache, animationCache: AnimationCache,
animationRenderer: MultiAnimationRenderer, animationRenderer: MultiAnimationRenderer,
content: Content, content: Content,
isVisibleForAnimations: Bool,
action: (() -> Void)?, action: (() -> Void)?,
longTapAction: (() -> Void)?, longTapAction: (() -> Void)?,
emojiFileUpdated: ((TelegramMediaFile?) -> Void)? = nil emojiFileUpdated: ((TelegramMediaFile?) -> Void)? = nil
@ -59,11 +69,25 @@ public final class EmojiStatusComponent: Component {
self.animationCache = animationCache self.animationCache = animationCache
self.animationRenderer = animationRenderer self.animationRenderer = animationRenderer
self.content = content self.content = content
self.isVisibleForAnimations = isVisibleForAnimations
self.action = action self.action = action
self.longTapAction = longTapAction self.longTapAction = longTapAction
self.emojiFileUpdated = emojiFileUpdated self.emojiFileUpdated = emojiFileUpdated
} }
public func withVisibleForAnimations(_ isVisibleForAnimations: Bool) -> EmojiStatusComponent {
return EmojiStatusComponent(
context: self.context,
animationCache: self.animationCache,
animationRenderer: self.animationRenderer,
content: self.content,
isVisibleForAnimations: isVisibleForAnimations,
action: self.action,
longTapAction: self.longTapAction,
emojiFileUpdated: self.emojiFileUpdated
)
}
public static func ==(lhs: EmojiStatusComponent, rhs: EmojiStatusComponent) -> Bool { public static func ==(lhs: EmojiStatusComponent, rhs: EmojiStatusComponent) -> Bool {
if lhs.context !== rhs.context { if lhs.context !== rhs.context {
return false return false
@ -77,23 +101,72 @@ public final class EmojiStatusComponent: Component {
if lhs.content != rhs.content { if lhs.content != rhs.content {
return false return false
} }
if lhs.isVisibleForAnimations != rhs.isVisibleForAnimations {
return false
}
return true return true
} }
public final class View: UIView { public final class View: UIView {
private final class AnimationFileProperties {
let path: String
let coloredComposition: Animation?
init(path: String, coloredComposition: Animation?) {
self.path = path
self.coloredComposition = coloredComposition
}
static func load(from path: String) -> AnimationFileProperties {
guard let size = fileSize(path), size < 1024 * 1024 else {
return AnimationFileProperties(path: path, coloredComposition: nil)
}
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
return AnimationFileProperties(path: path, coloredComposition: nil)
}
guard let unzippedData = TGGUnzipData(data, 1024 * 1024) else {
return AnimationFileProperties(path: path, coloredComposition: nil)
}
var coloredComposition: Animation?
if let composition = try? Animation.from(data: unzippedData) {
coloredComposition = composition
}
return AnimationFileProperties(path: path, coloredComposition: coloredComposition)
}
}
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
private var component: EmojiStatusComponent? private var component: EmojiStatusComponent?
private var iconView: UIImageView? private var iconView: UIImageView?
private var animationLayer: InlineStickerItemLayer? private var animationLayer: InlineStickerItemLayer?
private var lottieAnimationView: AnimationView?
private let hierarchyTrackingLayer: HierarchyTrackingLayer
private var emojiFile: TelegramMediaFile? private var emojiFile: TelegramMediaFile?
private var emojiFileDataProperties: AnimationFileProperties?
private var emojiFileDisposable: Disposable? private var emojiFileDisposable: Disposable?
private var emojiFileDataPathDisposable: Disposable?
override init(frame: CGRect) { override init(frame: CGRect) {
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
super.init(frame: frame) super.init(frame: frame)
self.layer.addSublayer(self.hierarchyTrackingLayer)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
self.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(_:)))) self.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(_:))))
self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
guard let strongSelf = self else {
return
}
if let lottieAnimationView = strongSelf.lottieAnimationView {
lottieAnimationView.play()
}
}
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -102,6 +175,7 @@ public final class EmojiStatusComponent: Component {
deinit { deinit {
self.emojiFileDisposable?.dispose() self.emojiFileDisposable?.dispose()
self.emojiFileDataPathDisposable?.dispose()
} }
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
@ -122,8 +196,11 @@ public final class EmojiStatusComponent: Component {
var iconImage: UIImage? var iconImage: UIImage?
var emojiFileId: Int64? var emojiFileId: Int64?
var emojiPlaceholderColor: UIColor? var emojiPlaceholderColor: UIColor?
var emojiThemeColor: UIColor?
var emojiLoopMode: LoopMode?
var emojiSize = CGSize() var emojiSize = CGSize()
//let previousContent = self.component?.content
if self.component?.content != component.content { if self.component?.content != component.content {
switch component.content { switch component.content {
case .none: case .none:
@ -155,6 +232,7 @@ public final class EmojiStatusComponent: Component {
context.fill(CGRect(origin: CGPoint(), size: size)) context.fill(CGRect(origin: CGPoint(), size: size))
context.restoreGState() context.restoreGState()
context.setBlendMode(.copy)
context.clip(to: CGRect(origin: .zero, size: size), mask: foregroundCgImage) context.clip(to: CGRect(origin: .zero, size: size), mask: foregroundCgImage)
context.setFillColor(foregroundColor.cgColor) context.setFillColor(foregroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size)) context.fill(CGRect(origin: CGPoint(), size: size))
@ -167,18 +245,23 @@ public final class EmojiStatusComponent: Component {
iconImage = nil iconImage = nil
case .scam: case .scam:
iconImage = nil iconImage = nil
case let .animation(animationContent, size, placeholderColor): case let .animation(animationContent, size, placeholderColor, themeColor, loopMode):
iconImage = nil iconImage = nil
emojiFileId = animationContent.fileId.id emojiFileId = animationContent.fileId.id
emojiPlaceholderColor = placeholderColor emojiPlaceholderColor = placeholderColor
emojiThemeColor = themeColor
emojiSize = size emojiSize = size
emojiLoopMode = loopMode
if case let .animation(previousAnimationContent, _, _) = self.component?.content { if case let .animation(previousAnimationContent, _, _, _, _) = self.component?.content {
if previousAnimationContent.fileId != animationContent.fileId { if previousAnimationContent.fileId != animationContent.fileId {
self.emojiFileDisposable?.dispose() self.emojiFileDisposable?.dispose()
self.emojiFileDisposable = nil self.emojiFileDisposable = nil
self.emojiFileDataPathDisposable?.dispose()
self.emojiFileDataPathDisposable = nil
self.emojiFile = nil self.emojiFile = nil
self.emojiFileDataProperties = nil
if let animationLayer = self.animationLayer { if let animationLayer = self.animationLayer {
self.animationLayer = nil self.animationLayer = nil
@ -192,6 +275,18 @@ public final class EmojiStatusComponent: Component {
animationLayer.removeFromSuperlayer() animationLayer.removeFromSuperlayer()
} }
} }
if let lottieAnimationView = self.lottieAnimationView {
self.lottieAnimationView = nil
if !transition.animation.isImmediate {
lottieAnimationView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak lottieAnimationView] _ in
lottieAnimationView?.removeFromSuperview()
})
lottieAnimationView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
} else {
lottieAnimationView.removeFromSuperview()
}
}
} }
} }
@ -204,9 +299,11 @@ public final class EmojiStatusComponent: Component {
} }
} else { } else {
iconImage = self.iconView?.image iconImage = self.iconView?.image
if case let .animation(animationContent, size, placeholderColor) = component.content { if case let .animation(animationContent, size, placeholderColor, themeColor, loopMode) = component.content {
emojiFileId = animationContent.fileId.id emojiFileId = animationContent.fileId.id
emojiPlaceholderColor = placeholderColor emojiPlaceholderColor = placeholderColor
emojiThemeColor = themeColor
emojiLoopMode = loopMode
emojiSize = size emojiSize = size
} }
} }
@ -248,17 +345,71 @@ public final class EmojiStatusComponent: Component {
} }
let emojiFileUpdated = component.emojiFileUpdated let emojiFileUpdated = component.emojiFileUpdated
if let emojiFileId = emojiFileId, let emojiPlaceholderColor = emojiPlaceholderColor { if let emojiFileId = emojiFileId, let emojiPlaceholderColor = emojiPlaceholderColor, let emojiLoopMode = emojiLoopMode {
size = availableSize size = availableSize
let _ = emojiLoopMode
if let emojiFile = self.emojiFile { if let emojiFile = self.emojiFile {
self.emojiFileDisposable?.dispose() self.emojiFileDisposable?.dispose()
self.emojiFileDisposable = nil self.emojiFileDisposable = nil
self.emojiFileDataPathDisposable?.dispose()
self.emojiFileDataPathDisposable = nil
/*if !"".isEmpty {
var resetThemeColor = false
let lottieAnimationView: AnimationView
if let current = self.lottieAnimationView {
lottieAnimationView = current
if case let .animation(_, _, _, previousThemeColor, _) = previousContent {
if previousThemeColor != emojiThemeColor {
resetThemeColor = true
}
} else {
resetThemeColor = true
}
} else {
resetThemeColor = true
lottieAnimationView = AnimationView(animation: coloredComposition)
lottieAnimationView.loopMode = .loop
self.lottieAnimationView = lottieAnimationView
self.addSubview(lottieAnimationView)
if !transition.animation.isImmediate {
lottieAnimationView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
lottieAnimationView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
}
}
if resetThemeColor {
for keypath in lottieAnimationView.allKeypaths(predicate: { $0.keys.contains(where: { $0.contains("_theme") }) && $0.keys.last == "Color" }) {
lottieAnimationView.setValueProvider(ColorValueProvider(emojiThemeColor.lottieColorValue), keypath: AnimationKeypath(keypath: keypath))
}
}
lottieAnimationView.frame = CGRect(origin: CGPoint(), size: size)
if component.isVisibleForAnimations {
if !lottieAnimationView.isAnimationPlaying {
lottieAnimationView.play()
}
} else {
if lottieAnimationView.isAnimationPlaying {
lottieAnimationView.stop()
}
}
} else {*/
let animationLayer: InlineStickerItemLayer let animationLayer: InlineStickerItemLayer
if let current = self.animationLayer { if let current = self.animationLayer {
animationLayer = current animationLayer = current
} else { } else {
let loopCount: Int?
switch emojiLoopMode {
case .forever:
loopCount = nil
case let .count(value):
loopCount = value
}
animationLayer = InlineStickerItemLayer( animationLayer = InlineStickerItemLayer(
context: component.context, context: component.context,
attemptSynchronousLoad: false, attemptSynchronousLoad: false,
@ -266,8 +417,10 @@ public final class EmojiStatusComponent: Component {
file: emojiFile, file: emojiFile,
cache: component.animationCache, cache: component.animationCache,
renderer: component.animationRenderer, renderer: component.animationRenderer,
unique: true,
placeholderColor: emojiPlaceholderColor, placeholderColor: emojiPlaceholderColor,
pointSize: emojiSize pointSize: emojiSize,
loopCount: loopCount
) )
self.animationLayer = animationLayer self.animationLayer = animationLayer
self.layer.addSublayer(animationLayer) self.layer.addSublayer(animationLayer)
@ -277,8 +430,68 @@ public final class EmojiStatusComponent: Component {
animationLayer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) animationLayer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
} }
} }
var accentTint = false
if let _ = emojiThemeColor {
for attribute in emojiFile.attributes {
if case let .CustomEmoji(_, _, packReference) = attribute {
switch packReference {
case let .id(id, _):
if id == 773947703670341676 {
accentTint = true
}
default:
break
}
}
}
}
if accentTint {
animationLayer.contentTintColor = emojiThemeColor
} else {
animationLayer.contentTintColor = nil
}
animationLayer.frame = CGRect(origin: CGPoint(), size: size) animationLayer.frame = CGRect(origin: CGPoint(), size: size)
animationLayer.isVisibleForAnimations = true animationLayer.isVisibleForAnimations = component.isVisibleForAnimations
/*} else {
if self.emojiFileDataPathDisposable == nil {
let account = component.context.account
self.emojiFileDataPathDisposable = (Signal<AnimationFileProperties?, NoError> { subscriber in
let disposable = MetaDisposable()
let _ = (account.postbox.mediaBox.resourceData(emojiFile.resource)
|> take(1)).start(next: { firstAttemptData in
if firstAttemptData.complete {
subscriber.putNext(AnimationFileProperties.load(from: firstAttemptData.path))
subscriber.putCompletion()
} else {
let fetchDisposable = freeMediaFileInteractiveFetched(account: account, fileReference: .standalone(media: emojiFile)).start()
let dataDisposable = account.postbox.mediaBox.resourceData(emojiFile.resource).start(next: { data in
if data.complete {
subscriber.putNext(AnimationFileProperties.load(from: data.path))
subscriber.putCompletion()
}
})
disposable.set(ActionDisposable {
fetchDisposable.dispose()
dataDisposable.dispose()
})
}
})
return disposable
}
|> deliverOnMainQueue).start(next: { [weak self] properties in
guard let strongSelf = self else {
return
}
strongSelf.emojiFileDataProperties = properties
strongSelf.state?.updated(transition: transition)
})
}
}*/
} else { } else {
if self.emojiFileDisposable == nil { if self.emojiFileDisposable == nil {
self.emojiFileDisposable = (component.context.engine.stickers.resolveInlineStickers(fileIds: [emojiFileId]) self.emojiFileDisposable = (component.context.engine.stickers.resolveInlineStickers(fileIds: [emojiFileId])
@ -287,6 +500,7 @@ public final class EmojiStatusComponent: Component {
return return
} }
strongSelf.emojiFile = result[emojiFileId] strongSelf.emojiFile = result[emojiFileId]
strongSelf.emojiFileDataProperties = nil
strongSelf.state?.updated(transition: transition) strongSelf.state?.updated(transition: transition)
emojiFileUpdated?(result[emojiFileId]) emojiFileUpdated?(result[emojiFileId])
@ -296,11 +510,14 @@ public final class EmojiStatusComponent: Component {
} else { } else {
if let _ = self.emojiFile { if let _ = self.emojiFile {
self.emojiFile = nil self.emojiFile = nil
self.emojiFileDataProperties = nil
emojiFileUpdated?(nil) emojiFileUpdated?(nil)
} }
self.emojiFileDisposable?.dispose() self.emojiFileDisposable?.dispose()
self.emojiFileDisposable = nil self.emojiFileDisposable = nil
self.emojiFileDataPathDisposable?.dispose()
self.emojiFileDataPathDisposable = nil
if let animationLayer = self.animationLayer { if let animationLayer = self.animationLayer {
self.animationLayer = nil self.animationLayer = nil
@ -314,6 +531,18 @@ public final class EmojiStatusComponent: Component {
animationLayer.removeFromSuperlayer() animationLayer.removeFromSuperlayer()
} }
} }
if let lottieAnimationView = self.lottieAnimationView {
self.lottieAnimationView = nil
if !transition.animation.isImmediate {
lottieAnimationView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak lottieAnimationView] _ in
lottieAnimationView?.removeFromSuperview()
})
lottieAnimationView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
} else {
lottieAnimationView.removeFromSuperview()
}
}
} }
return size return size

View File

@ -19,6 +19,42 @@ import TextFormat
import AppBundle import AppBundle
import GZip import GZip
private func randomGenericReactionEffect(context: AccountContext) -> Signal<String?, NoError> {
return context.engine.stickers.loadedStickerPack(reference: .emojiGenericAnimations, forceActualized: false)
|> map { result -> [TelegramMediaFile]? in
switch result {
case let .result(_, items, _):
return items.map(\.file)
default:
return nil
}
}
|> filter { $0 != nil }
|> take(1)
|> mapToSignal { items -> Signal<String?, NoError> in
guard let items = items else {
return .single(nil)
}
guard let file = items.randomElement() else {
return .single(nil)
}
return Signal { subscriber in
let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start()
let dataDisposable = (context.account.postbox.mediaBox.resourceData(file.resource)
|> filter(\.complete)
|> take(1)).start(next: { data in
subscriber.putNext(data.path)
subscriber.putCompletion()
})
return ActionDisposable {
fetchDisposable.dispose()
dataDisposable.dispose()
}
}
}
}
public final class EmojiStatusSelectionComponent: Component { public final class EmojiStatusSelectionComponent: Component {
public typealias EnvironmentType = Empty public typealias EnvironmentType = Empty
@ -192,6 +228,9 @@ public final class EmojiStatusSelectionController: ViewController {
private var availableReactions: AvailableReactions? private var availableReactions: AvailableReactions?
private var availableReactionsDisposable: Disposable? private var availableReactionsDisposable: Disposable?
private var genericReactionEffectDisposable: Disposable?
private var genericReactionEffect: String?
private var hapticFeedback: HapticFeedback? private var hapticFeedback: HapticFeedback?
private var isDismissed: Bool = false private var isDismissed: Bool = false
@ -310,11 +349,17 @@ public final class EmojiStatusSelectionController: ViewController {
} }
strongSelf.availableReactions = availableReactions strongSelf.availableReactions = availableReactions
}) })
self.genericReactionEffectDisposable = (randomGenericReactionEffect(context: context)
|> deliverOnMainQueue).start(next: { [weak self] path in
self?.genericReactionEffect = path
})
} }
deinit { deinit {
self.emojiContentDisposable?.dispose() self.emojiContentDisposable?.dispose()
self.availableReactionsDisposable?.dispose() self.availableReactionsDisposable?.dispose()
self.genericReactionEffectDisposable?.dispose()
} }
private func refreshLayout(transition: Transition) { private func refreshLayout(transition: Transition) {
@ -373,7 +418,17 @@ public final class EmojiStatusSelectionController: ViewController {
view.isOpaque = false view.isOpaque = false
effectView = view effectView = view
} else if let itemFile = item.itemFile, let url = getAppBundle().url(forResource: "generic_reaction_small_effect", withExtension: "json"), let composition = Animation.filepath(url.path) { } else if let itemFile = item.itemFile {
var effectData: Data?
if let genericReactionEffect = self.genericReactionEffect, let data = try? Data(contentsOf: URL(fileURLWithPath: genericReactionEffect)) {
effectData = TGGUnzipData(data, 5 * 1024 * 1024) ?? data
} else {
if let url = getAppBundle().url(forResource: "generic_reaction_small_effect", withExtension: "json") {
effectData = try? Data(contentsOf: url)
}
}
if let effectData = effectData, let composition = try? Animation.from(data: effectData) {
let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable)) let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
view.animationSpeed = 1.0 view.animationSpeed = 1.0
view.backgroundColor = nil view.backgroundColor = nil
@ -395,6 +450,9 @@ public final class EmojiStatusSelectionController: ViewController {
placeholderColor: UIColor(white: 0.0, alpha: 0.0), placeholderColor: UIColor(white: 0.0, alpha: 0.0),
pointSize: CGSize(width: 32.0, height: 32.0) pointSize: CGSize(width: 32.0, height: 32.0)
) )
if item.accentTint {
baseItemLayer.contentTintColor = self.presentationData.theme.list.itemAccentColor
}
if let sublayers = animationLayer.sublayers { if let sublayers = animationLayer.sublayers {
for sublayer in sublayers { for sublayer in sublayers {
@ -410,6 +468,7 @@ public final class EmojiStatusSelectionController: ViewController {
effectView = view effectView = view
} }
}
if let sourceCopyLayer = sourceLayer.snapshotContentTree() { if let sourceCopyLayer = sourceLayer.snapshotContentTree() {
self.layer.addSublayer(sourceCopyLayer) self.layer.addSublayer(sourceCopyLayer)

View File

@ -83,7 +83,9 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
private let emoji: ChatTextInputTextCustomEmojiAttribute private let emoji: ChatTextInputTextCustomEmojiAttribute
private let cache: AnimationCache private let cache: AnimationCache
private let renderer: MultiAnimationRenderer private let renderer: MultiAnimationRenderer
private let unique: Bool
private let placeholderColor: UIColor private let placeholderColor: UIColor
private let loopCount: Int?
private let pointSize: CGSize private let pointSize: CGSize
private let pixelSize: CGSize private let pixelSize: CGSize
@ -96,6 +98,16 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
private var fetchDisposable: Disposable? private var fetchDisposable: Disposable?
private var loadDisposable: Disposable? private var loadDisposable: Disposable?
public var contentTintColor: UIColor? {
didSet {
if self.contentTintColor != oldValue {
self.updateTintColor()
}
}
}
private var currentLoopCount: Int = 0
private var isInHierarchyValue: Bool = false private var isInHierarchyValue: Bool = false
public var isVisibleForAnimations: Bool = false { public var isVisibleForAnimations: Bool = false {
didSet { didSet {
@ -105,12 +117,14 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
} }
} }
public init(context: AccountContext, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor, pointSize: CGSize) { public init(context: AccountContext, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool = false, placeholderColor: UIColor, pointSize: CGSize, loopCount: Int? = nil) {
self.context = context self.context = context
self.emoji = emoji self.emoji = emoji
self.cache = cache self.cache = cache
self.renderer = renderer self.renderer = renderer
self.unique = unique
self.placeholderColor = placeholderColor self.placeholderColor = placeholderColor
self.loopCount = loopCount
let scale = min(2.0, UIScreenScale) let scale = min(2.0, UIScreenScale)
self.pointSize = pointSize self.pointSize = pointSize
@ -159,10 +173,28 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
return nullAction return nullAction
} }
private func updatePlayback() { private func updateTintColor() {
let shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations if !self.isDisplayingPlaceholder {
self.layerTintColor = self.contentTintColor?.cgColor
} else {
self.layerTintColor = nil
}
}
private func updatePlayback() {
var shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations
if shouldBePlaying, let loopCount = self.loopCount, self.currentLoopCount >= loopCount {
shouldBePlaying = false
}
if self.shouldBeAnimating != shouldBePlaying {
self.shouldBeAnimating = shouldBePlaying self.shouldBeAnimating = shouldBePlaying
if !shouldBePlaying {
self.currentLoopCount = 0
}
}
} }
private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) { private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) {
@ -177,7 +209,10 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: self.pointSize, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: self.placeholderColor) { if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: self.pointSize, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: self.placeholderColor) {
self.contents = image.cgImage self.contents = image.cgImage
self.isDisplayingPlaceholder = true self.isDisplayingPlaceholder = true
self.updateTintColor()
} }
} else {
self.updateTintColor()
} }
self.loadAnimation() self.loadAnimation()
@ -197,6 +232,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
if let image = image { if let image = image {
strongSelf.contents = image.cgImage strongSelf.contents = image.cgImage
strongSelf.isDisplayingPlaceholder = true strongSelf.isDisplayingPlaceholder = true
strongSelf.updateTintColor()
} }
if isFinal { if isFinal {
@ -224,9 +260,9 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
if file.isAnimatedSticker || file.isVideoEmoji { if file.isAnimatedSticker || file.isVideoEmoji {
let keyframeOnly = self.pixelSize.width >= 120.0 let keyframeOnly = self.pixelSize.width >= 120.0
self.disposable = renderer.add(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: animationCacheFetchFile(context: context, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: keyframeOnly)) self.disposable = renderer.add(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, unique: self.unique, size: self.pixelSize, fetch: animationCacheFetchFile(context: context, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: keyframeOnly))
} else { } else {
self.disposable = renderer.add(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: { options in self.disposable = renderer.add(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, unique: self.unique, size: self.pixelSize, fetch: { options in
let dataDisposable = context.account.postbox.mediaBox.resourceData(file.resource).start(next: { result in let dataDisposable = context.account.postbox.mediaBox.resourceData(file.resource).start(next: { result in
guard result.complete else { guard result.complete else {
return return
@ -250,11 +286,13 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
return return
} }
self.isDisplayingPlaceholder = displayPlaceholder self.isDisplayingPlaceholder = displayPlaceholder
self.updateTintColor()
} }
override public func transitionToContents(_ contents: AnyObject) { override public func transitionToContents(_ contents: AnyObject, didLoop: Bool) {
if self.isDisplayingPlaceholder { if self.isDisplayingPlaceholder {
self.isDisplayingPlaceholder = false self.isDisplayingPlaceholder = false
self.updateTintColor()
if let current = self.contents { if let current = self.contents {
let previousLayer = SimpleLayer() let previousLayer = SimpleLayer()
@ -275,6 +313,13 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
} else { } else {
self.contents = contents self.contents = contents
} }
if didLoop {
self.currentLoopCount += 1
if let loopCount = self.loopCount, self.currentLoopCount >= loopCount {
self.updatePlayback()
}
}
} }
} }

View File

@ -1455,7 +1455,7 @@ private final class GroupExpandActionButton: UIButton {
let textConstrainedWidth: CGFloat = 100.0 let textConstrainedWidth: CGFloat = 100.0
let color = theme.list.itemCheckColors.foregroundColor let color = theme.list.itemCheckColors.foregroundColor
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor.cgColor self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlVibrantOverlayColor.cgColor
self.tintContainerLayer.backgroundColor = UIColor.white.cgColor self.tintContainerLayer.backgroundColor = UIColor.white.cgColor
let textSize: CGSize let textSize: CGSize
@ -1662,19 +1662,22 @@ public final class EmojiPagerContentComponent: Component {
public let itemFile: TelegramMediaFile? public let itemFile: TelegramMediaFile?
public let subgroupId: Int32? public let subgroupId: Int32?
public let icon: Icon public let icon: Icon
public let accentTint: Bool
public init( public init(
animationData: EntityKeyboardAnimationData?, animationData: EntityKeyboardAnimationData?,
content: ItemContent, content: ItemContent,
itemFile: TelegramMediaFile?, itemFile: TelegramMediaFile?,
subgroupId: Int32?, subgroupId: Int32?,
icon: Icon icon: Icon,
accentTint: Bool
) { ) {
self.animationData = animationData self.animationData = animationData
self.content = content self.content = content
self.itemFile = itemFile self.itemFile = itemFile
self.subgroupId = subgroupId self.subgroupId = subgroupId
self.icon = icon self.icon = icon
self.accentTint = accentTint
} }
public static func ==(lhs: Item, rhs: Item) -> Bool { public static func ==(lhs: Item, rhs: Item) -> Bool {
@ -1696,6 +1699,9 @@ public final class EmojiPagerContentComponent: Component {
if lhs.icon != rhs.icon { if lhs.icon != rhs.icon {
return false return false
} }
if lhs.accentTint != rhs.accentTint {
return false
}
return true return true
} }
@ -2252,7 +2258,7 @@ public final class EmojiPagerContentComponent: Component {
return return
} }
strongSelf.disposable = renderer.add(target: strongSelf, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: pixelSize.width >= 120.0)) strongSelf.disposable = renderer.add(target: strongSelf, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, unique: false, size: pixelSize, fetch: animationCacheFetchFile(context: context, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: pixelSize.width >= 120.0))
} }
if attemptSynchronousLoad { if attemptSynchronousLoad {
@ -2440,7 +2446,7 @@ public final class EmojiPagerContentComponent: Component {
self.onUpdateDisplayPlaceholder(displayPlaceholder, 0.0) self.onUpdateDisplayPlaceholder(displayPlaceholder, 0.0)
} }
public override func transitionToContents(_ contents: AnyObject) { public override func transitionToContents(_ contents: AnyObject, didLoop: Bool) {
self.contents = contents self.contents = contents
if self.displayPlaceholder { if self.displayPlaceholder {
@ -3995,6 +4001,12 @@ public final class EmojiPagerContentComponent: Component {
} }
itemLayer.update(transition: transition, size: itemFrame.size, badge: badge, blurredBadgeColor: UIColor(white: 0.0, alpha: 0.1), blurredBadgeBackgroundColor: keyboardChildEnvironment.theme.list.plainBackgroundColor) itemLayer.update(transition: transition, size: itemFrame.size, badge: badge, blurredBadgeColor: UIColor(white: 0.0, alpha: 0.1), blurredBadgeBackgroundColor: keyboardChildEnvironment.theme.list.plainBackgroundColor)
if item.accentTint {
itemLayer.layerTintColor = keyboardChildEnvironment.theme.list.itemAccentColor.cgColor
} else {
itemLayer.layerTintColor = nil
}
if let placeholderView = self.visibleItemPlaceholderViews[itemId] { if let placeholderView = self.visibleItemPlaceholderViews[itemId] {
if placeholderView.layer.position != itemPosition || placeholderView.layer.bounds != itemBounds { if placeholderView.layer.position != itemPosition || placeholderView.layer.bounds != itemBounds {
itemTransition.setFrame(view: placeholderView, frame: itemFrame) itemTransition.setFrame(view: placeholderView, frame: itemFrame)
@ -4019,7 +4031,7 @@ public final class EmojiPagerContentComponent: Component {
self.visibleItemSelectionLayers[itemId] = itemSelectionLayer self.visibleItemSelectionLayers[itemId] = itemSelectionLayer
} }
itemSelectionLayer.backgroundColor = keyboardChildEnvironment.theme.chat.inputMediaPanel.panelContentVibrantOverlayColor.cgColor itemSelectionLayer.backgroundColor = keyboardChildEnvironment.theme.chat.inputMediaPanel.panelContentControlVibrantOverlayColor.cgColor
itemSelectionLayer.tintContainerLayer.backgroundColor = UIColor.white.cgColor itemSelectionLayer.tintContainerLayer.backgroundColor = UIColor.white.cgColor
itemSelectionLayer.frame = baseItemFrame itemSelectionLayer.frame = baseItemFrame
} }
@ -4798,7 +4810,8 @@ public final class EmojiPagerContentComponent: Component {
content: .icon(.premiumStar), content: .icon(.premiumStar),
itemFile: nil, itemFile: nil,
subgroupId: nil, subgroupId: nil,
icon: .none icon: .none,
accentTint: false
) )
let groupId = "recent" let groupId = "recent"
@ -4822,6 +4835,20 @@ public final class EmojiPagerContentComponent: Component {
} }
existingIds.insert(file.fileId) existingIds.insert(file.fileId)
var accentTint = false
for attribute in file.attributes {
if case let .CustomEmoji(_, _, packReference) = attribute {
switch packReference {
case let .id(id, _):
if id == 773947703670341676 {
accentTint = true
}
default:
break
}
}
}
let resultItem: EmojiPagerContentComponent.Item let resultItem: EmojiPagerContentComponent.Item
let animationData = EntityKeyboardAnimationData(file: file) let animationData = EntityKeyboardAnimationData(file: file)
@ -4830,7 +4857,8 @@ public final class EmojiPagerContentComponent: Component {
content: .animation(animationData), content: .animation(animationData),
itemFile: file, itemFile: file,
subgroupId: nil, subgroupId: nil,
icon: .none icon: .none,
accentTint: accentTint
) )
if let groupIndex = itemGroupIndexById[groupId] { if let groupIndex = itemGroupIndexById[groupId] {
@ -4852,13 +4880,28 @@ public final class EmojiPagerContentComponent: Component {
let resultItem: EmojiPagerContentComponent.Item let resultItem: EmojiPagerContentComponent.Item
var accentTint = false
for attribute in file.attributes {
if case let .CustomEmoji(_, _, packReference) = attribute {
switch packReference {
case let .id(id, _):
if id == 773947703670341676 {
accentTint = true
}
default:
break
}
}
}
let animationData = EntityKeyboardAnimationData(file: file) let animationData = EntityKeyboardAnimationData(file: file)
resultItem = EmojiPagerContentComponent.Item( resultItem = EmojiPagerContentComponent.Item(
animationData: animationData, animationData: animationData,
content: .animation(animationData), content: .animation(animationData),
itemFile: file, itemFile: file,
subgroupId: nil, subgroupId: nil,
icon: .none icon: .none,
accentTint: accentTint
) )
if let groupIndex = itemGroupIndexById[groupId] { if let groupIndex = itemGroupIndexById[groupId] {
@ -4891,6 +4934,13 @@ public final class EmojiPagerContentComponent: Component {
} }
} }
let maxTopLineCount: Int
if hasPremium {
maxTopLineCount = 2
} else {
maxTopLineCount = 5
}
for reactionItem in topReactionItems { for reactionItem in topReactionItems {
if existingIds.contains(reactionItem.reaction) { if existingIds.contains(reactionItem.reaction) {
continue continue
@ -4911,14 +4961,15 @@ public final class EmojiPagerContentComponent: Component {
content: .animation(animationData), content: .animation(animationData),
itemFile: animationFile, itemFile: animationFile,
subgroupId: nil, subgroupId: nil,
icon: icon icon: icon,
accentTint: false
) )
let groupId = "recent" let groupId = "recent"
if let groupIndex = itemGroupIndexById[groupId] { if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem) itemGroups[groupIndex].items.append(resultItem)
if itemGroups[groupIndex].items.count >= 8 * 2 { if itemGroups[groupIndex].items.count >= 8 * maxTopLineCount {
break break
} }
} else { } else {
@ -4966,7 +5017,8 @@ public final class EmojiPagerContentComponent: Component {
content: .animation(animationData), content: .animation(animationData),
itemFile: animationFile, itemFile: animationFile,
subgroupId: nil, subgroupId: nil,
icon: icon icon: icon,
accentTint: false
) )
if hasPremium { if hasPremium {
@ -5040,7 +5092,8 @@ public final class EmojiPagerContentComponent: Component {
content: .animation(animationData), content: .animation(animationData),
itemFile: animationFile, itemFile: animationFile,
subgroupId: nil, subgroupId: nil,
icon: icon icon: icon,
accentTint: false
) )
let groupId = "popular" let groupId = "popular"
@ -5082,7 +5135,8 @@ public final class EmojiPagerContentComponent: Component {
content: .animation(animationData), content: .animation(animationData),
itemFile: file, itemFile: file,
subgroupId: nil, subgroupId: nil,
icon: .none icon: .none,
accentTint: false
) )
case let .text(text): case let .text(text):
resultItem = EmojiPagerContentComponent.Item( resultItem = EmojiPagerContentComponent.Item(
@ -5090,7 +5144,8 @@ public final class EmojiPagerContentComponent: Component {
content: .staticEmoji(text), content: .staticEmoji(text),
itemFile: nil, itemFile: nil,
subgroupId: nil, subgroupId: nil,
icon: .none icon: .none,
accentTint: false
) )
} }
@ -5113,7 +5168,8 @@ public final class EmojiPagerContentComponent: Component {
content: .staticEmoji(emojiString), content: .staticEmoji(emojiString),
itemFile: nil, itemFile: nil,
subgroupId: subgroupId.rawValue, subgroupId: subgroupId.rawValue,
icon: .none icon: .none,
accentTint: false
) )
if let groupIndex = itemGroupIndexById[groupId] { if let groupIndex = itemGroupIndexById[groupId] {
@ -5148,7 +5204,8 @@ public final class EmojiPagerContentComponent: Component {
content: .animation(animationData), content: .animation(animationData),
itemFile: item.file, itemFile: item.file,
subgroupId: nil, subgroupId: nil,
icon: icon icon: icon,
accentTint: false
) )
let supergroupId = entry.index.collectionId let supergroupId = entry.index.collectionId
@ -5208,7 +5265,8 @@ public final class EmojiPagerContentComponent: Component {
content: .animation(animationData), content: .animation(animationData),
itemFile: item.file, itemFile: item.file,
subgroupId: nil, subgroupId: nil,
icon: .none icon: .none,
accentTint: false
) )
let supergroupId = featuredEmojiPack.info.id let supergroupId = featuredEmojiPack.info.id

View File

@ -115,7 +115,8 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
content: .animation(component.item), content: .animation(component.item),
itemFile: nil, itemFile: nil,
subgroupId: nil, subgroupId: nil,
icon: .none icon: .none,
accentTint: false
), ),
context: component.context, context: component.context,
attemptSynchronousLoad: false, attemptSynchronousLoad: false,

View File

@ -398,7 +398,7 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
let preferredRowAlignment = self.preferredRowAlignment let preferredRowAlignment = self.preferredRowAlignment
return LoadFrameTask(task: { [weak self] in return LoadFrameTask(task: { [weak self] in
let frame = item.advance(advance: frameAdvance, requestedFormat: .yuva(rowAlignment: preferredRowAlignment)) let frame = item.advance(advance: frameAdvance, requestedFormat: .yuva(rowAlignment: preferredRowAlignment))?.frame
let textureY = readyTextureY ?? TextureStoragePool.takeNew(device: device, parameters: fullParameters, pool: texturePoolFullPlane) let textureY = readyTextureY ?? TextureStoragePool.takeNew(device: device, parameters: fullParameters, pool: texturePoolFullPlane)
let textureU = readyTextureU ?? TextureStoragePool.takeNew(device: device, parameters: halfParameters, pool: texturePoolHalfPlane) let textureU = readyTextureU ?? TextureStoragePool.takeNew(device: device, parameters: halfParameters, pool: texturePoolHalfPlane)
@ -517,7 +517,7 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
return nullAction return nullAction
} }
func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable? { func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable? {
if size != self.cellSize { if size != self.cellSize {
return nil return nil
} }
@ -798,13 +798,13 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
self.isPlaying = isPlaying self.isPlaying = isPlaying
} }
public func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable { public func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable {
assert(Thread.isMainThread) assert(Thread.isMainThread)
let alignedSize = CGSize(width: CGFloat(alignUp(size: Int(size.width), align: 16)), height: CGFloat(alignUp(size: Int(size.height), align: 16))) let alignedSize = CGSize(width: CGFloat(alignUp(size: Int(size.width), align: 16)), height: CGFloat(alignUp(size: Int(size.height), align: 16)))
for (_, surfaceLayer) in self.surfaceLayers { for (_, surfaceLayer) in self.surfaceLayers {
if let disposable = surfaceLayer.add(target: target, cache: cache, itemId: itemId, size: alignedSize, fetch: fetch) { if let disposable = surfaceLayer.add(target: target, cache: cache, itemId: itemId, unique: unique, size: alignedSize, fetch: fetch) {
return disposable return disposable
} }
} }
@ -818,7 +818,7 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
strongSelf.updateIsPlaying() strongSelf.updateIsPlaying()
}) })
self.surfaceLayers[index] = surfaceLayer self.surfaceLayers[index] = surfaceLayer
if let disposable = surfaceLayer.add(target: target, cache: cache, itemId: itemId, size: alignedSize, fetch: fetch) { if let disposable = surfaceLayer.add(target: target, cache: cache, itemId: itemId, unique: unique, size: alignedSize, fetch: fetch) {
return disposable return disposable
} else { } else {
return EmptyDisposable return EmptyDisposable

View File

@ -6,7 +6,7 @@ import AnimationCache
import Accelerate import Accelerate
public protocol MultiAnimationRenderer: AnyObject { public protocol MultiAnimationRenderer: AnyObject {
func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable
func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool
func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable
func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage)
@ -73,7 +73,7 @@ open class MultiAnimationRenderTarget: SimpleLayer {
open func updateDisplayPlaceholder(displayPlaceholder: Bool) { open func updateDisplayPlaceholder(displayPlaceholder: Bool) {
} }
open func transitionToContents(_ contents: AnyObject) { open func transitionToContents(_ contents: AnyObject, didLoop: Bool) {
} }
} }
@ -281,7 +281,7 @@ private final class ItemAnimationContext {
for i in 0 ... index { for i in 0 ... index {
let result = item.advance(advance: .frames(1), requestedFormat: .rgba) let result = item.advance(advance: .frames(1), requestedFormat: .rgba)
if i == index { if i == index {
return result return result?.frame
} }
} }
return nil return nil
@ -294,7 +294,7 @@ private final class ItemAnimationContext {
for target in self.targets.copyItems() { for target in self.targets.copyItems() {
if let target = target.value { if let target = target.value {
target.transitionToContents(currentFrame.image.cgImage!) target.transitionToContents(currentFrame.image.cgImage!, didLoop: false)
if let blurredRepresentationTarget = target.blurredRepresentationTarget { if let blurredRepresentationTarget = target.blurredRepresentationTarget {
blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage
@ -305,7 +305,7 @@ private final class ItemAnimationContext {
} else { } else {
for target in self.targets.copyItems() { for target in self.targets.copyItems() {
if let target = target.value { if let target = target.value {
target.transitionToContents(placeholder.cgImage!) target.transitionToContents(placeholder.cgImage!, didLoop: false)
} }
} }
@ -316,7 +316,7 @@ private final class ItemAnimationContext {
func updateAddedTarget(target: MultiAnimationRenderTarget) { func updateAddedTarget(target: MultiAnimationRenderTarget) {
if let currentFrame = self.currentFrame { if let currentFrame = self.currentFrame {
if let cgImage = currentFrame.image.cgImage { if let cgImage = currentFrame.image.cgImage {
target.transitionToContents(cgImage) target.transitionToContents(cgImage, didLoop: false)
if let blurredRepresentationTarget = target.blurredRepresentationTarget { if let blurredRepresentationTarget = target.blurredRepresentationTarget {
blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage
@ -384,10 +384,16 @@ private final class ItemAnimationContext {
self.loadingFrameTaskId = taskId self.loadingFrameTaskId = taskId
return LoadFrameGroupTask(task: { [weak self] in return LoadFrameGroupTask(task: { [weak self] in
let currentFrame: Frame? let currentFrame: (frame: Frame, didLoop: Bool)?
do { do {
if let frame = try item.tryWith({ $0.advance(advance: frameAdvance, requestedFormat: .rgba) }) { if let (frame, didLoop) = try item.tryWith({ item -> (AnimationCacheItemFrame, Bool)? in
currentFrame = Frame(frame: frame) if let result = item.advance(advance: frameAdvance, requestedFormat: .rgba) {
return (result.frame, result.didLoop)
} else {
return nil
}
}), let mappedFrame = Frame(frame: frame) {
currentFrame = (mappedFrame, didLoop)
} else { } else {
currentFrame = nil currentFrame = nil
} }
@ -408,13 +414,13 @@ private final class ItemAnimationContext {
strongSelf.loadingFrameTaskId = nil strongSelf.loadingFrameTaskId = nil
if let currentFrame = currentFrame { if let currentFrame = currentFrame {
strongSelf.currentFrame = currentFrame strongSelf.currentFrame = currentFrame.frame
for target in strongSelf.targets.copyItems() { for target in strongSelf.targets.copyItems() {
if let target = target.value { if let target = target.value {
target.transitionToContents(currentFrame.image.cgImage!) target.transitionToContents(currentFrame.frame.image.cgImage!, didLoop: currentFrame.didLoop)
if let blurredRepresentationTarget = target.blurredRepresentationTarget { if let blurredRepresentationTarget = target.blurredRepresentationTarget {
blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage blurredRepresentationTarget.contents = currentFrame.frame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage
} }
} }
} }
@ -444,10 +450,12 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
var id: String var id: String
var width: Int var width: Int
var height: Int var height: Int
var uniqueId: Int
} }
private var itemContexts: [ItemKey: ItemAnimationContext] = [:] private var itemContexts: [ItemKey: ItemAnimationContext] = [:]
private var nextQueueAffinity: Int = 0 private var nextQueueAffinity: Int = 0
private var nextUniqueId: Int = 1
private(set) var isPlaying: Bool = false { private(set) var isPlaying: Bool = false {
didSet { didSet {
@ -462,8 +470,14 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
self.stateUpdated = stateUpdated self.stateUpdated = stateUpdated
} }
func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable { func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable {
let itemKey = ItemKey(id: itemId, width: Int(size.width), height: Int(size.height)) var uniqueId = 0
if unique {
uniqueId = self.nextUniqueId
self.nextUniqueId += 1
}
let itemKey = ItemKey(id: itemId, width: Int(size.width), height: Int(size.height), uniqueId: uniqueId)
let itemContext: ItemAnimationContext let itemContext: ItemAnimationContext
if let current = self.itemContexts[itemKey] { if let current = self.itemContexts[itemKey] {
itemContext = current itemContext = current
@ -521,7 +535,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
guard let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) else { guard let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) else {
return false return false
} }
guard let loadedFrame = ItemAnimationContext.Frame(frame: frame) else { guard let loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) else {
return false return false
} }
@ -551,7 +565,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
let loadedFrame: ItemAnimationContext.Frame? let loadedFrame: ItemAnimationContext.Frame?
if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) {
loadedFrame = ItemAnimationContext.Frame(frame: frame) loadedFrame = ItemAnimationContext.Frame(frame: frame.frame)
} else { } else {
loadedFrame = nil loadedFrame = nil
} }
@ -564,7 +578,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
if let loadedFrame = loadedFrame { if let loadedFrame = loadedFrame {
if let cgImage = loadedFrame.image.cgImage { if let cgImage = loadedFrame.image.cgImage {
if hadIntermediateUpdate { if hadIntermediateUpdate {
target.transitionToContents(cgImage) target.transitionToContents(cgImage, didLoop: false)
} else { } else {
target.contents = cgImage target.contents = cgImage
} }
@ -583,7 +597,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
} }
func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) {
if let itemContext = self.itemContexts[ItemKey(id: itemId, width: Int(size.width), height: Int(size.height))] { if let itemContext = self.itemContexts[ItemKey(id: itemId, width: Int(size.width), height: Int(size.height), uniqueId: 0)] {
itemContext.setFrameIndex(index: frameIndex, placeholder: placeholder) itemContext.setFrameIndex(index: frameIndex, placeholder: placeholder)
} }
} }
@ -664,7 +678,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
} }
} }
public func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable { public func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable {
let groupContext: GroupContext let groupContext: GroupContext
if let current = self.groupContext { if let current = self.groupContext {
groupContext = current groupContext = current
@ -678,7 +692,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
self.groupContext = groupContext self.groupContext = groupContext
} }
let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch) let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, unique: unique, size: size, fetch: fetch)
return ActionDisposable { return ActionDisposable {
disposable.dispose() disposable.dispose()

View File

@ -1667,7 +1667,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
if let reactionItem = reactionItem { if let reactionItem = reactionItem {
let standaloneReactionAnimation = StandaloneReactionAnimation() let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: strongSelf.chatDisplayNode.historyNode.takeGenericReactionEffect())
strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
@ -6733,7 +6733,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
if reaction.value == updatedReaction { if reaction.value == updatedReaction {
let standaloneReactionAnimation = StandaloneReactionAnimation() let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: strongSelf.chatDisplayNode.historyNode.takeGenericReactionEffect())
strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)

View File

@ -201,7 +201,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
content: .animation(animationData), content: .animation(animationData),
itemFile: item.file, itemFile: item.file,
subgroupId: nil, subgroupId: nil,
icon: .none icon: .none,
accentTint: false
) )
let supergroupId = "featuredTop" let supergroupId = "featuredTop"
@ -238,7 +239,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
content: .animation(animationData), content: .animation(animationData),
itemFile: item.file, itemFile: item.file,
subgroupId: nil, subgroupId: nil,
icon: .none icon: .none,
accentTint: false
) )
let groupId = "saved" let groupId = "saved"
@ -266,7 +268,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
content: .animation(animationData), content: .animation(animationData),
itemFile: item.media, itemFile: item.media,
subgroupId: nil, subgroupId: nil,
icon: .none icon: .none,
accentTint: false
) )
let groupId = "recent" let groupId = "recent"
@ -317,7 +320,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
content: .animation(animationData), content: .animation(animationData),
itemFile: item.file, itemFile: item.file,
subgroupId: nil, subgroupId: nil,
icon: .none icon: .none,
accentTint: false
) )
let groupId = "premium" let groupId = "premium"
@ -350,7 +354,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
content: .animation(animationData), content: .animation(animationData),
itemFile: item.file, itemFile: item.file,
subgroupId: nil, subgroupId: nil,
icon: .none icon: .none,
accentTint: false
) )
let groupId = "peerSpecific" let groupId = "peerSpecific"
@ -373,7 +378,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
content: .animation(animationData), content: .animation(animationData),
itemFile: item.file, itemFile: item.file,
subgroupId: nil, subgroupId: nil,
icon: .none icon: .none,
accentTint: false
) )
let groupId = entry.index.collectionId let groupId = entry.index.collectionId
if let groupIndex = itemGroupIndexById[groupId] { if let groupIndex = itemGroupIndexById[groupId] {
@ -426,7 +432,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
content: .animation(animationData), content: .animation(animationData),
itemFile: item.file, itemFile: item.file,
subgroupId: nil, subgroupId: nil,
icon: .none icon: .none,
accentTint: false
) )
let supergroupId = featuredStickerPack.info.id let supergroupId = featuredStickerPack.info.id

View File

@ -585,6 +585,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
private var refreshDisplayedItemRangeTimer: SwiftSignalKit.Timer? private var refreshDisplayedItemRangeTimer: SwiftSignalKit.Timer?
private var genericReactionEffect: String?
private var genericReactionEffectDisposable: Disposable?
private var visibleMessageRange = Atomic<VisibleMessageRange?>(value: nil) private var visibleMessageRange = Atomic<VisibleMessageRange?>(value: nil)
private let clientId: Atomic<Int32> private let clientId: Atomic<Int32>
@ -1568,6 +1571,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
return strongSelf.isSelectionGestureEnabled return strongSelf.isSelectionGestureEnabled
} }
self.view.addGestureRecognizer(selectionRecognizer) self.view.addGestureRecognizer(selectionRecognizer)
self.loadNextGenericReactionEffect(context: context)
} }
deinit { deinit {
@ -1579,6 +1584,24 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
self.loadedMessagesFromCachedDataDisposable?.dispose() self.loadedMessagesFromCachedDataDisposable?.dispose()
self.preloadAdPeerDisposable.dispose() self.preloadAdPeerDisposable.dispose()
self.refreshDisplayedItemRangeTimer?.invalidate() self.refreshDisplayedItemRangeTimer?.invalidate()
self.genericReactionEffectDisposable?.dispose()
}
func takeGenericReactionEffect() -> String? {
let result = self.genericReactionEffect
self.loadNextGenericReactionEffect(context: self.context)
return result
}
private func loadNextGenericReactionEffect(context: AccountContext) {
self.genericReactionEffectDisposable?.dispose()
self.genericReactionEffectDisposable = (ReactionContextNode.randomGenericReactionEffect(context: context) |> deliverOnMainQueue).start(next: { [weak self] path in
guard let strongSelf = self else {
return
}
strongSelf.genericReactionEffect = path
})
} }
public func setLoadStateUpdated(_ f: @escaping (ChatHistoryNodeLoadState, Bool) -> Void) { public func setLoadStateUpdated(_ f: @escaping (ChatHistoryNodeLoadState, Bool) -> Void) {
@ -2724,7 +2747,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
} }
if reaction.value == updatedReaction { if reaction.value == updatedReaction {
let standaloneReactionAnimation = StandaloneReactionAnimation() let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: self.genericReactionEffect)
chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)

View File

@ -26,6 +26,8 @@ import ChatPresentationInterfaceState
import ChatMessageBackground import ChatMessageBackground
import AnimationCache import AnimationCache
import MultiAnimationRenderer import MultiAnimationRenderer
import ComponentFlow
import EmojiStatusComponent
enum InternalBubbleTapAction { enum InternalBubbleTapAction {
case action(() -> Void) case action(() -> Void)
@ -488,7 +490,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
private var nameNode: TextNode? private var nameNode: TextNode?
private var adminBadgeNode: TextNode? private var adminBadgeNode: TextNode?
private var credibilityIconNode: ASImageNode? private var credibilityIconView: ComponentHostView<Empty>?
private var credibilityIconComponent: EmojiStatusComponent?
private var forwardInfoNode: ChatMessageForwardInfoNode? private var forwardInfoNode: ChatMessageForwardInfoNode?
var forwardInfoReferenceNode: ASDisplayNode? { var forwardInfoReferenceNode: ASDisplayNode? {
return self.forwardInfoNode return self.forwardInfoNode
@ -532,6 +535,23 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
if let replyInfoNode = self.replyInfoNode { if let replyInfoNode = self.replyInfoNode {
replyInfoNode.visibility = self.visibility != .none replyInfoNode.visibility = self.visibility != .none
} }
self.visibilityStatus = self.visibility != .none
}
}
}
private var visibilityStatus: Bool = false {
didSet {
if self.visibilityStatus != oldValue {
if let credibilityIconView = self.credibilityIconView, let credibilityIconComponent = self.credibilityIconComponent {
let _ = credibilityIconView.update(
transition: .immediate,
component: AnyComponent(credibilityIconComponent.withVisibleForAnimations(self.visibilityStatus)),
environment: {},
containerSize: credibilityIconView.bounds.size
)
}
} }
} }
} }
@ -1527,7 +1547,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
bottomNodeMergeStatus = .Both bottomNodeMergeStatus = .Both
} }
var currentCredibilityIconImage: UIImage? var currentCredibilityIcon: EmojiStatusComponent.Content?
var initialDisplayHeader = true var initialDisplayHeader = true
if let backgroundHiding = backgroundHiding, case .always = backgroundHiding { if let backgroundHiding = backgroundHiding, case .always = backgroundHiding {
@ -1557,11 +1577,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
} }
if case let .peer(peerId) = item.chatLocation, let authorPeerId = item.message.author?.id, authorPeerId == peerId { if case let .peer(peerId) = item.chatLocation, let authorPeerId = item.message.author?.id, authorPeerId == peerId {
} else if effectiveAuthor.isScam { } else if effectiveAuthor.isScam {
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme.theme, strings: item.presentationData.strings, type: incoming ? .regular : .outgoing) currentCredibilityIcon = .scam(color: incoming ? item.presentationData.theme.theme.chat.message.incoming.scamColor : item.presentationData.theme.theme.chat.message.outgoing.scamColor)
} else if effectiveAuthor.isFake { } else if effectiveAuthor.isFake {
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme.theme, strings: item.presentationData.strings, type: incoming ? .regular : .outgoing) currentCredibilityIcon = .fake(color: incoming ? item.presentationData.theme.theme.chat.message.incoming.scamColor : item.presentationData.theme.theme.chat.message.outgoing.scamColor)
} else if let user = item.message.author as? TelegramUser, let emojiStatus = user.emojiStatus {
currentCredibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor, themeColor: authorNameColor?.withMultipliedAlpha(0.4), loopMode: .count(2))
} }
} }
@ -1756,8 +1777,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
} }
var credibilityIconWidth: CGFloat = 0.0 var credibilityIconWidth: CGFloat = 0.0
if let credibilityIconImage = currentCredibilityIconImage { if let currentCredibilityIcon = currentCredibilityIcon {
credibilityIconWidth += credibilityIconImage.size.width + 4.0 credibilityIconWidth += 4.0
switch currentCredibilityIcon {
case .fake, .scam:
credibilityIconWidth += 30.0
default:
credibilityIconWidth += 20.0
}
} }
let adminBadgeSizeAndApply = adminBadgeLayout(TextNodeLayoutArguments(attributedString: adminBadgeString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0, maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let adminBadgeSizeAndApply = adminBadgeLayout(TextNodeLayoutArguments(attributedString: adminBadgeString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0, maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
adminNodeSizeApply = (adminBadgeSizeAndApply.0.size, { adminNodeSizeApply = (adminBadgeSizeAndApply.0.size, {
@ -2249,7 +2276,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
contentOrigin: contentOrigin, contentOrigin: contentOrigin,
nameNodeOriginY: nameNodeOriginY, nameNodeOriginY: nameNodeOriginY,
layoutConstants: layoutConstants, layoutConstants: layoutConstants,
currentCredibilityIconImage: currentCredibilityIconImage, currentCredibilityIcon: currentCredibilityIcon,
adminNodeSizeApply: adminNodeSizeApply, adminNodeSizeApply: adminNodeSizeApply,
contentUpperRightCorner: contentUpperRightCorner, contentUpperRightCorner: contentUpperRightCorner,
forwardInfoSizeApply: forwardInfoSizeApply, forwardInfoSizeApply: forwardInfoSizeApply,
@ -2293,7 +2320,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
contentOrigin: CGPoint, contentOrigin: CGPoint,
nameNodeOriginY: CGFloat, nameNodeOriginY: CGFloat,
layoutConstants: ChatMessageItemLayoutConstants, layoutConstants: ChatMessageItemLayoutConstants,
currentCredibilityIconImage: UIImage?, currentCredibilityIcon: EmojiStatusComponent.Content?,
adminNodeSizeApply: (CGSize, () -> TextNode?), adminNodeSizeApply: (CGSize, () -> TextNode?),
contentUpperRightCorner: CGPoint, contentUpperRightCorner: CGPoint,
forwardInfoSizeApply: (CGSize, (CGFloat) -> ChatMessageForwardInfoNode?), forwardInfoSizeApply: (CGSize, (CGFloat) -> ChatMessageForwardInfoNode?),
@ -2411,20 +2438,38 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
animation.animator.updateFrame(layer: nameNode.layer, frame: nameNodeFrame, completion: nil) animation.animator.updateFrame(layer: nameNode.layer, frame: nameNodeFrame, completion: nil)
} }
if let credibilityIconImage = currentCredibilityIconImage { if let currentCredibilityIcon = currentCredibilityIcon {
let credibilityIconNode: ASImageNode let credibilityIconView: ComponentHostView<Empty>
if let node = strongSelf.credibilityIconNode { if let current = strongSelf.credibilityIconView {
credibilityIconNode = node credibilityIconView = current
} else { } else {
credibilityIconNode = ASImageNode() credibilityIconView = ComponentHostView<Empty>()
strongSelf.credibilityIconNode = credibilityIconNode credibilityIconView.isUserInteractionEnabled = false
strongSelf.clippingNode.addSubnode(credibilityIconNode) strongSelf.credibilityIconView = credibilityIconView
strongSelf.clippingNode.view.addSubview(credibilityIconView)
} }
credibilityIconNode.frame = CGRect(origin: CGPoint(x: nameNode.frame.maxX + 4.0, y: nameNode.frame.minY), size: credibilityIconImage.size)
credibilityIconNode.image = credibilityIconImage let credibilityIconComponent = EmojiStatusComponent(
context: item.context,
animationCache: item.context.animationCache,
animationRenderer: item.context.animationRenderer,
content: currentCredibilityIcon,
isVisibleForAnimations: strongSelf.visibilityStatus,
action: nil,
longTapAction: nil
)
let credibilityIconSize = credibilityIconView.update(
transition: .immediate,
component: AnyComponent(credibilityIconComponent),
environment: {},
containerSize: CGSize(width: 20.0, height: 20.0)
)
credibilityIconView.frame = CGRect(origin: CGPoint(x: nameNode.frame.maxX + 4.0, y: nameNode.frame.minY + floor((nameNode.bounds.height - credibilityIconSize.height) / 2.0)), size: credibilityIconSize)
} else { } else {
strongSelf.credibilityIconNode?.removeFromSupernode() strongSelf.credibilityIconView?.removeFromSuperview()
strongSelf.credibilityIconNode = nil strongSelf.credibilityIconView = nil
} }
if let adminBadgeNode = adminNodeSizeApply.1() { if let adminBadgeNode = adminNodeSizeApply.1() {

View File

@ -679,7 +679,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
case .scam: case .scam:
titleCredibilityContent = .scam(color: self.theme.chat.message.incoming.scamColor) titleCredibilityContent = .scam(color: self.theme.chat.message.incoming.scamColor)
case let .emojiStatus(emojiStatus): case let .emojiStatus(emojiStatus):
titleCredibilityContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: self.theme.list.mediaPlaceholderColor) titleCredibilityContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: self.theme.list.mediaPlaceholderColor, themeColor: self.theme.list.itemAccentColor, loopMode: .count(2))
} }
let titleCredibilitySize = self.titleCredibilityIconView.update( let titleCredibilitySize = self.titleCredibilityIconView.update(
@ -689,6 +689,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
animationCache: self.animationCache, animationCache: self.animationCache,
animationRenderer: self.animationRenderer, animationRenderer: self.animationRenderer,
content: titleCredibilityContent, content: titleCredibilityContent,
isVisibleForAnimations: true,
action: nil, action: nil,
longTapAction: nil longTapAction: nil
)), )),

View File

@ -221,6 +221,8 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode {
self.addSubnode(itemNode) self.addSubnode(itemNode)
} }
itemNode.visibility = .visible(1.0, .infinite)
let height = itemNode.contentSize.height let height = itemNode.contentSize.height
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: itemNode.bounds.size)) transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: itemNode.bounds.size))

View File

@ -2354,8 +2354,8 @@ final class PeerInfoHeaderNode: ASDisplayNode {
emojiExpandedStatusContent = .scam(color: presentationData.theme.chat.message.incoming.scamColor) emojiExpandedStatusContent = .scam(color: presentationData.theme.chat.message.incoming.scamColor)
case let .emojiStatus(emojiStatus): case let .emojiStatus(emojiStatus):
currentEmojiStatus = emojiStatus currentEmojiStatus = emojiStatus
emojiRegularStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: presentationData.theme.list.mediaPlaceholderColor) emojiRegularStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: presentationData.theme.list.mediaPlaceholderColor, themeColor: presentationData.theme.list.itemAccentColor, loopMode: .forever)
emojiExpandedStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: UIColor(rgb: 0xffffff, alpha: 0.15)) emojiExpandedStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: UIColor(rgb: 0xffffff, alpha: 0.15), themeColor: presentationData.theme.list.itemAccentColor, loopMode: .forever)
} }
let animateStatusIcon = !self.titleCredibilityIconView.bounds.isEmpty let animateStatusIcon = !self.titleCredibilityIconView.bounds.isEmpty
@ -2367,6 +2367,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
animationCache: self.animationCache, animationCache: self.animationCache,
animationRenderer: self.animationRenderer, animationRenderer: self.animationRenderer,
content: emojiRegularStatusContent, content: emojiRegularStatusContent,
isVisibleForAnimations: true,
action: { [weak self] in action: { [weak self] in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
@ -2427,6 +2428,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
animationCache: self.animationCache, animationCache: self.animationCache,
animationRenderer: self.animationRenderer, animationRenderer: self.animationRenderer,
content: emojiExpandedStatusContent, content: emojiExpandedStatusContent,
isVisibleForAnimations: true,
action: { [weak self] in action: { [weak self] in
guard let strongSelf = self else { guard let strongSelf = self else {
return return