mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Emoji status and reaction improvements
This commit is contained in:
parent
5ca7417ee1
commit
b924ea326e
@ -118,7 +118,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
|
||||
case .premium:
|
||||
statusContent = .premium(color: self.theme.list.itemAccentColor)
|
||||
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
|
||||
@ -144,6 +144,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
|
||||
animationCache: self.animationCache,
|
||||
animationRenderer: self.animationRenderer,
|
||||
content: statusContent,
|
||||
isVisibleForAnimations: true,
|
||||
action: { [weak self] in
|
||||
guard let strongSelf = self, let titleCredibilityIconView = strongSelf.titleCredibilityIconView else {
|
||||
return
|
||||
@ -351,7 +352,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
|
||||
case .premium:
|
||||
statusContent = .premium(color: self.theme.list.itemAccentColor)
|
||||
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)
|
||||
@ -372,6 +373,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
|
||||
animationCache: self.animationCache,
|
||||
animationRenderer: self.animationRenderer,
|
||||
content: statusContent,
|
||||
isVisibleForAnimations: true,
|
||||
action: { [weak self] in
|
||||
guard let strongSelf = self, let titleCredibilityIconView = strongSelf.titleCredibilityIconView else {
|
||||
return
|
||||
|
@ -460,6 +460,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
let pinnedIconNode: ASImageNode
|
||||
var secretIconNode: ASImageNode?
|
||||
var credibilityIconView: ComponentHostView<Empty>?
|
||||
var credibilityIconComponent: EmojiStatusComponent?
|
||||
let mutedIconNode: ASImageNode
|
||||
|
||||
private var hierarchyTrackingLayer: HierarchyTrackingLayer?
|
||||
@ -634,6 +635,15 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
self.updateVideoVisibility()
|
||||
|
||||
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, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
if let peer = messages.last?.author {
|
||||
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 {
|
||||
currentCredibilityIconContent = .scam(color: item.presentationData.theme.chat.message.incoming.scamColor)
|
||||
} else if peer.isFake {
|
||||
@ -1537,7 +1547,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
} else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer {
|
||||
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 {
|
||||
currentCredibilityIconContent = .scam(color: item.presentationData.theme.chat.message.incoming.scamColor)
|
||||
} else if peer.isFake {
|
||||
@ -2056,16 +2066,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
strongSelf.credibilityIconView = credibilityIconView
|
||||
strongSelf.contextContainer.view.addSubview(credibilityIconView)
|
||||
}
|
||||
|
||||
let credibilityIconComponent = EmojiStatusComponent(
|
||||
context: item.context,
|
||||
animationCache: item.interaction.animationCache,
|
||||
animationRenderer: item.interaction.animationRenderer,
|
||||
content: currentCredibilityIconContent,
|
||||
isVisibleForAnimations: strongSelf.visibilityStatus,
|
||||
action: nil,
|
||||
longTapAction: nil
|
||||
)
|
||||
strongSelf.credibilityIconComponent = credibilityIconComponent
|
||||
|
||||
let iconSize = credibilityIconView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(EmojiStatusComponent(
|
||||
context: item.context,
|
||||
animationCache: item.interaction.animationCache,
|
||||
animationRenderer: item.interaction.animationRenderer,
|
||||
content: currentCredibilityIconContent,
|
||||
action: nil,
|
||||
longTapAction: nil
|
||||
)),
|
||||
component: AnyComponent(credibilityIconComponent),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 20.0, height: 20.0)
|
||||
)
|
||||
|
@ -33,6 +33,13 @@ public final class ReactionIconView: PortalSourceView {
|
||||
|
||||
private var disposable: Disposable?
|
||||
|
||||
public var iconFrame: CGRect? {
|
||||
if let animationLayer = self.animationLayer {
|
||||
return animationLayer.frame
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
@ -370,6 +370,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
private let avatarNode: AvatarNode
|
||||
private let titleNode: TextNode
|
||||
private var credibilityIconView: ComponentHostView<Empty>?
|
||||
private var credibilityIconComponent: EmojiStatusComponent?
|
||||
private let statusNode: TextNode
|
||||
private var badgeBackgroundNode: ASImageNode?
|
||||
private var badgeTextNode: TextNode?
|
||||
@ -397,6 +398,37 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
public var item: ContactsPeerItem? {
|
||||
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() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
@ -616,7 +648,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
} else if peer.isFake {
|
||||
credibilityIcon = .fake(color: item.presentationData.theme.chat.message.incoming.scamColor)
|
||||
} 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 {
|
||||
credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||
} else if peer.isPremium && !premiumConfiguration.isPremiumDisabled {
|
||||
@ -987,17 +1019,21 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
strongSelf.credibilityIconView = credibilityIconView
|
||||
}
|
||||
|
||||
let credibilityIconComponent = EmojiStatusComponent(
|
||||
context: item.context,
|
||||
animationCache: animationCache,
|
||||
animationRenderer: animationRenderer,
|
||||
content: credibilityIcon,
|
||||
isVisibleForAnimations: strongSelf.visibilityStatus,
|
||||
action: nil,
|
||||
longTapAction: nil,
|
||||
emojiFileUpdated: nil
|
||||
)
|
||||
strongSelf.credibilityIconComponent = credibilityIconComponent
|
||||
|
||||
let iconSize = credibilityIconView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(EmojiStatusComponent(
|
||||
context: item.context,
|
||||
animationCache: animationCache,
|
||||
animationRenderer: animationRenderer,
|
||||
content: credibilityIcon,
|
||||
action: nil,
|
||||
longTapAction: nil,
|
||||
emojiFileUpdated: nil
|
||||
)),
|
||||
component: AnyComponent(credibilityIconComponent),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 20.0, height: 20.0)
|
||||
)
|
||||
|
@ -412,6 +412,7 @@ private func makeSubtreeSnapshot(layer: CALayer, keepTransform: Bool = false) ->
|
||||
view.layer.contentsCenter = layer.contentsCenter
|
||||
view.layer.contentsGravity = layer.contentsGravity
|
||||
view.layer.masksToBounds = layer.masksToBounds
|
||||
view.layer.layerTintColor = layer.layerTintColor
|
||||
if let mask = layer.mask {
|
||||
if let shapeMask = mask as? CAShapeLayer {
|
||||
let maskLayer = CAShapeLayer()
|
||||
@ -424,8 +425,11 @@ private func makeSubtreeSnapshot(layer: CALayer, keepTransform: Bool = false) ->
|
||||
maskLayer.contentsScale = mask.contentsScale
|
||||
maskLayer.contentsCenter = mask.contentsCenter
|
||||
maskLayer.contentsGravity = mask.contentsGravity
|
||||
maskLayer.frame = mask.frame
|
||||
maskLayer.transform = mask.transform
|
||||
maskLayer.position = mask.position
|
||||
maskLayer.bounds = mask.bounds
|
||||
maskLayer.anchorPoint = mask.anchorPoint
|
||||
maskLayer.layerTintColor = mask.layerTintColor
|
||||
view.layer.mask = maskLayer
|
||||
}
|
||||
}
|
||||
@ -438,10 +442,17 @@ private func makeSubtreeSnapshot(layer: CALayer, keepTransform: Bool = false) ->
|
||||
if keepTransform {
|
||||
subtree.layer.transform = sublayer.transform
|
||||
}
|
||||
subtree.frame = sublayer.frame
|
||||
subtree.bounds = sublayer.bounds
|
||||
subtree.layer.transform = sublayer.transform
|
||||
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 {
|
||||
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)
|
||||
} else {
|
||||
@ -467,13 +478,15 @@ private func makeLayerSubtreeSnapshot(layer: CALayer) -> CALayer? {
|
||||
view.masksToBounds = layer.masksToBounds
|
||||
view.cornerRadius = layer.cornerRadius
|
||||
view.backgroundColor = layer.backgroundColor
|
||||
view.layerTintColor = layer.layerTintColor
|
||||
if let sublayers = layer.sublayers {
|
||||
for sublayer in sublayers {
|
||||
let subtree = makeLayerSubtreeSnapshot(layer: sublayer)
|
||||
if let subtree = subtree {
|
||||
subtree.transform = sublayer.transform
|
||||
subtree.frame = sublayer.frame
|
||||
subtree.position = sublayer.position
|
||||
subtree.bounds = sublayer.bounds
|
||||
subtree.anchorPoint = sublayer.anchorPoint
|
||||
layer.addSublayer(subtree)
|
||||
} else {
|
||||
return nil
|
||||
@ -498,13 +511,16 @@ private func makeLayerSubtreeSnapshotAsView(layer: CALayer) -> UIView? {
|
||||
view.layer.masksToBounds = layer.masksToBounds
|
||||
view.layer.cornerRadius = layer.cornerRadius
|
||||
view.layer.backgroundColor = layer.backgroundColor
|
||||
view.layer.layerTintColor = layer.layerTintColor
|
||||
if let sublayers = layer.sublayers {
|
||||
for sublayer in sublayers {
|
||||
let subtree = makeLayerSubtreeSnapshotAsView(layer: sublayer)
|
||||
if let subtree = subtree {
|
||||
subtree.layer.transform = sublayer.transform
|
||||
subtree.layer.frame = sublayer.frame
|
||||
subtree.layer.position = sublayer.position
|
||||
subtree.layer.bounds = sublayer.bounds
|
||||
subtree.layer.anchorPoint = sublayer.anchorPoint
|
||||
subtree.layer.layerTintColor = sublayer.layerTintColor
|
||||
view.addSubview(subtree)
|
||||
} else {
|
||||
return nil
|
||||
@ -526,8 +542,9 @@ public extension UIView {
|
||||
self.isHidden = true
|
||||
}
|
||||
if let snapshot = snapshot {
|
||||
snapshot.frame = self.frame
|
||||
snapshot.bounds = self.bounds
|
||||
snapshot.layer.position = self.layer.position
|
||||
snapshot.layer.bounds = self.layer.bounds
|
||||
snapshot.layer.anchorPoint = self.layer.anchorPoint
|
||||
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 {
|
||||
func snapshotContentTreeAsView(unhide: Bool = false) -> UIView? {
|
||||
let wasHidden = self.isHidden
|
||||
|
@ -462,6 +462,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
||||
private let labelBadgeNode: ASImageNode
|
||||
private var labelArrowNode: ASImageNode?
|
||||
private let statusNode: TextNode
|
||||
private var credibilityIconComponent: EmojiStatusComponent?
|
||||
private var credibilityIconView: ComponentHostView<Empty>?
|
||||
private var switchNode: SwitchNode?
|
||||
private var checkNode: ASImageNode?
|
||||
@ -474,6 +475,37 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
||||
|
||||
private var editableControlNode: ItemListEditableControlNode?
|
||||
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 {
|
||||
if self.editableControlNode != nil || self.disabledOverlayNode != nil {
|
||||
@ -614,7 +646,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
||||
} else if item.peer.isFake {
|
||||
credibilityIcon = .fake(color: item.presentationData.theme.chat.message.incoming.scamColor)
|
||||
} 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 {
|
||||
credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||
} else if item.peer.isPremium && !premiumConfiguration.isPremiumDisabled {
|
||||
@ -1085,17 +1117,20 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
||||
strongSelf.credibilityIconView = credibilityIconView
|
||||
}
|
||||
|
||||
let credibilityIconComponent = EmojiStatusComponent(
|
||||
context: item.context,
|
||||
animationCache: animationCache,
|
||||
animationRenderer: animationRenderer,
|
||||
content: credibilityIcon,
|
||||
isVisibleForAnimations: strongSelf.visibilityStatus,
|
||||
action: nil,
|
||||
longTapAction: nil,
|
||||
emojiFileUpdated: nil
|
||||
)
|
||||
strongSelf.credibilityIconComponent = credibilityIconComponent
|
||||
let iconSize = credibilityIconView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(EmojiStatusComponent(
|
||||
context: item.context,
|
||||
animationCache: animationCache,
|
||||
animationRenderer: animationRenderer,
|
||||
content: credibilityIcon,
|
||||
action: nil,
|
||||
longTapAction: nil,
|
||||
emojiFileUpdated: nil
|
||||
)),
|
||||
component: AnyComponent(credibilityIconComponent),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 20.0, height: 20.0)
|
||||
)
|
||||
|
@ -20,12 +20,12 @@ private enum PeerReactionsMode {
|
||||
|
||||
private final class PeerAllowedReactionListControllerArguments {
|
||||
let context: AccountContext
|
||||
let setMode: (PeerReactionsMode) -> Void
|
||||
let setMode: (PeerReactionsMode, Bool) -> Void
|
||||
let toggleItem: (MessageReaction.Reaction) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
setMode: @escaping (PeerReactionsMode) -> Void,
|
||||
setMode: @escaping (PeerReactionsMode, Bool) -> Void,
|
||||
toggleItem: @escaping (MessageReaction.Reaction) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
@ -41,6 +41,7 @@ private enum PeerAllowedReactionListControllerSection: Int32 {
|
||||
|
||||
private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
|
||||
enum StableId: Hashable {
|
||||
case allowSwitch
|
||||
case allowAllHeader
|
||||
case allowAll
|
||||
case allowSome
|
||||
@ -50,6 +51,7 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
|
||||
case item(MessageReaction.Reaction)
|
||||
}
|
||||
|
||||
case allowSwitch(text: String, value: Bool)
|
||||
case allowAllHeader(String)
|
||||
case allowAll(text: String, isEnabled: Bool)
|
||||
case allowSome(text: String, isEnabled: Bool)
|
||||
@ -57,11 +59,11 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
|
||||
case allowAllInfo(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 {
|
||||
switch self {
|
||||
case .allowAllHeader, .allowAll, .allowSome, .allowNone, .allowAllInfo:
|
||||
case .allowSwitch, .allowAllHeader, .allowAll, .allowSome, .allowNone, .allowAllInfo:
|
||||
return PeerAllowedReactionListControllerSection.all.rawValue
|
||||
case .itemsHeader, .item:
|
||||
return PeerAllowedReactionListControllerSection.items.rawValue
|
||||
@ -70,6 +72,8 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
|
||||
|
||||
var stableId: StableId {
|
||||
switch self {
|
||||
case .allowSwitch:
|
||||
return .allowSwitch
|
||||
case .allowAllHeader:
|
||||
return .allowAllHeader
|
||||
case .allowAll:
|
||||
@ -82,32 +86,40 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
|
||||
return .allowAllInfo
|
||||
case .itemsHeader:
|
||||
return .itemsHeader
|
||||
case let .item(_, value, _, _, _, _):
|
||||
case let .item(_, value, _, _, _, _, _):
|
||||
return .item(value)
|
||||
}
|
||||
}
|
||||
|
||||
var sortId: Int {
|
||||
switch self {
|
||||
case .allowAllHeader:
|
||||
case .allowSwitch:
|
||||
return 0
|
||||
case .allowAll:
|
||||
case .allowAllHeader:
|
||||
return 1
|
||||
case .allowSome:
|
||||
case .allowAll:
|
||||
return 2
|
||||
case .allowNone:
|
||||
case .allowSome:
|
||||
return 3
|
||||
case .allowAllInfo:
|
||||
case .allowNone:
|
||||
return 4
|
||||
case .itemsHeader:
|
||||
case .allowAllInfo:
|
||||
return 5
|
||||
case let .item(index, _, _, _, _, _):
|
||||
case .itemsHeader:
|
||||
return 6
|
||||
case let .item(index, _, _, _, _, _, _):
|
||||
return 100 + index
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: PeerAllowedReactionListControllerEntry, rhs: PeerAllowedReactionListControllerEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .allowSwitch(text, value):
|
||||
if case .allowSwitch(text, value) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .allowAllHeader(text):
|
||||
if case .allowAllHeader(text) = rhs {
|
||||
return true
|
||||
@ -144,8 +156,8 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .item(index, value, availableReactions, reaction, text, isEnabled):
|
||||
if case .item(index, value, availableReactions, reaction, text, isEnabled) = rhs {
|
||||
case let .item(index, value, availableReactions, reaction, text, isEnabled, allDisabled):
|
||||
if case .item(index, value, availableReactions, reaction, text, isEnabled, allDisabled) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -160,6 +172,14 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! PeerAllowedReactionListControllerArguments
|
||||
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):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .allowAll(text, isEnabled):
|
||||
@ -177,7 +197,7 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
|
||||
zeroSeparatorInsets: false,
|
||||
sectionId: self.section,
|
||||
action: {
|
||||
arguments.setMode(.all)
|
||||
arguments.setMode(.all, true)
|
||||
},
|
||||
deleteAction: nil
|
||||
)
|
||||
@ -196,7 +216,7 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
|
||||
zeroSeparatorInsets: false,
|
||||
sectionId: self.section,
|
||||
action: {
|
||||
arguments.setMode(.some)
|
||||
arguments.setMode(.some, true)
|
||||
},
|
||||
deleteAction: nil
|
||||
)
|
||||
@ -215,7 +235,7 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
|
||||
zeroSeparatorInsets: false,
|
||||
sectionId: self.section,
|
||||
action: {
|
||||
arguments.setMode(.empty)
|
||||
arguments.setMode(.empty, true)
|
||||
},
|
||||
deleteAction: nil
|
||||
)
|
||||
@ -223,7 +243,7 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .itemsHeader(text):
|
||||
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(
|
||||
context: arguments.context,
|
||||
presentationData: presentationData,
|
||||
@ -231,6 +251,7 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
|
||||
reaction: reaction,
|
||||
title: text,
|
||||
value: isEnabled,
|
||||
enabled: !allDisabled,
|
||||
sectionId: self.section,
|
||||
style: .blocks,
|
||||
updated: { _ in
|
||||
@ -255,48 +276,63 @@ private func peerAllowedReactionListControllerEntries(
|
||||
) -> [PeerAllowedReactionListControllerEntry] {
|
||||
var entries: [PeerAllowedReactionListControllerEntry] = []
|
||||
|
||||
if let availableReactions = availableReactions, let allowedReactions = state.updatedAllowedReactions, let mode = state.updatedMode {
|
||||
//TODO:localize
|
||||
entries.append(.allowAllHeader("AVAILABLE REACTIONS"))
|
||||
|
||||
//TODO:localize
|
||||
entries.append(.allowAll(text: "All Reactions", isEnabled: mode == .all))
|
||||
entries.append(.allowSome(text: "Some Reactions", isEnabled: mode == .some))
|
||||
entries.append(.allowNone(text: "No Reactions", isEnabled: mode == .empty))
|
||||
|
||||
let allInfoText: String
|
||||
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
|
||||
switch mode {
|
||||
case .all:
|
||||
allInfoText = "Subscribers of this channel can use any emoji as reactions to messages."
|
||||
case .some:
|
||||
allInfoText = "You can select emoji that will allow subscribers of your channel to react to messages."
|
||||
case .empty:
|
||||
allInfoText = "Subscribers of the channel can't add any reactions to messages."
|
||||
}
|
||||
} else {
|
||||
switch mode {
|
||||
case .all:
|
||||
allInfoText = "Members of this group can use any emoji as reactions to messages."
|
||||
case .some:
|
||||
allInfoText = "You can select emoji that will allow members of your group to react to messages."
|
||||
case .empty:
|
||||
allInfoText = "Members of the group can't add any reactions to messages."
|
||||
}
|
||||
}
|
||||
|
||||
entries.append(.allowAllInfo(allInfoText))
|
||||
|
||||
if mode == .some {
|
||||
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)))
|
||||
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
|
||||
entries.append(.allowAllHeader("AVAILABLE REACTIONS"))
|
||||
|
||||
//TODO:localize
|
||||
entries.append(.allowAll(text: "All Reactions", isEnabled: mode == .all))
|
||||
entries.append(.allowSome(text: "Some Reactions", isEnabled: mode == .some))
|
||||
entries.append(.allowNone(text: "No Reactions", isEnabled: mode == .empty))
|
||||
|
||||
let allInfoText: String
|
||||
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
|
||||
switch mode {
|
||||
case .all:
|
||||
allInfoText = "Subscribers of this channel can use any emoji as reactions to messages."
|
||||
case .some:
|
||||
allInfoText = "You can select emoji that will allow subscribers of your channel to react to messages."
|
||||
case .empty:
|
||||
allInfoText = "Subscribers of the channel can't add any reactions to messages."
|
||||
}
|
||||
} else {
|
||||
switch mode {
|
||||
case .all:
|
||||
allInfoText = "Members of this group can use any emoji as reactions to messages."
|
||||
case .some:
|
||||
allInfoText = "Members of the group can use only some allowed emoji as reactions to messages."
|
||||
case .empty:
|
||||
allInfoText = "Members of the group can't add any reactions to messages."
|
||||
}
|
||||
}
|
||||
|
||||
entries.append(.allowAllInfo(allInfoText))
|
||||
|
||||
if mode == .some {
|
||||
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: false))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -330,7 +366,11 @@ public func peerAllowedReactionListController(
|
||||
switch value {
|
||||
case .all:
|
||||
state.updatedMode = .all
|
||||
state.updatedAllowedReactions = Set()
|
||||
if let availableReactions = availableReactions {
|
||||
state.updatedAllowedReactions = Set(availableReactions.reactions.filter(\.isEnabled).map(\.value))
|
||||
} else {
|
||||
state.updatedAllowedReactions = Set()
|
||||
}
|
||||
case let .limited(reactions):
|
||||
state.updatedMode = .some
|
||||
state.updatedAllowedReactions = Set(reactions)
|
||||
@ -346,7 +386,7 @@ public func peerAllowedReactionListController(
|
||||
|
||||
let arguments = PeerAllowedReactionListControllerArguments(
|
||||
context: context,
|
||||
setMode: { mode in
|
||||
setMode: { mode, resetItems in
|
||||
let _ = (context.engine.stickers.availableReactions()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { availableReactions in
|
||||
@ -360,23 +400,37 @@ public func peerAllowedReactionListController(
|
||||
if var updatedAllowedReactions = state.updatedAllowedReactions {
|
||||
switch mode {
|
||||
case .all:
|
||||
updatedAllowedReactions.removeAll()
|
||||
for availableReaction in availableReactions.reactions {
|
||||
if !availableReaction.isEnabled {
|
||||
continue
|
||||
if resetItems {
|
||||
updatedAllowedReactions.removeAll()
|
||||
for availableReaction in availableReactions.reactions {
|
||||
if !availableReaction.isEnabled {
|
||||
continue
|
||||
}
|
||||
updatedAllowedReactions.insert(availableReaction.value)
|
||||
}
|
||||
updatedAllowedReactions.insert(availableReaction.value)
|
||||
}
|
||||
case .some:
|
||||
updatedAllowedReactions.removeAll()
|
||||
if let thumbsUp = availableReactions.reactions.first(where: { $0.value == .builtin("👍") }) {
|
||||
updatedAllowedReactions.insert(thumbsUp.value)
|
||||
}
|
||||
if let thumbsDown = availableReactions.reactions.first(where: { $0.value == .builtin("👎") }) {
|
||||
updatedAllowedReactions.insert(thumbsDown.value)
|
||||
if resetItems {
|
||||
updatedAllowedReactions.removeAll()
|
||||
if let thumbsUp = availableReactions.reactions.first(where: { $0.value == .builtin("👍") }) {
|
||||
updatedAllowedReactions.insert(thumbsUp.value)
|
||||
}
|
||||
if let thumbsDown = availableReactions.reactions.first(where: { $0.value == .builtin("👎") }) {
|
||||
updatedAllowedReactions.insert(thumbsDown.value)
|
||||
}
|
||||
} else {
|
||||
updatedAllowedReactions.removeAll()
|
||||
for availableReaction in availableReactions.reactions {
|
||||
if !availableReaction.isEnabled {
|
||||
continue
|
||||
}
|
||||
updatedAllowedReactions.insert(availableReaction.value)
|
||||
}
|
||||
}
|
||||
case .empty:
|
||||
updatedAllowedReactions.removeAll()
|
||||
if resetItems {
|
||||
updatedAllowedReactions.removeAll()
|
||||
}
|
||||
}
|
||||
state.updatedAllowedReactions = updatedAllowedReactions
|
||||
}
|
||||
@ -391,6 +445,9 @@ public func peerAllowedReactionListController(
|
||||
if var updatedAllowedReactions = state.updatedAllowedReactions {
|
||||
if updatedAllowedReactions.contains(reaction) {
|
||||
updatedAllowedReactions.remove(reaction)
|
||||
if state.updatedMode == .all {
|
||||
state.updatedMode = .some
|
||||
}
|
||||
} else {
|
||||
updatedAllowedReactions.insert(reaction)
|
||||
}
|
||||
@ -446,8 +503,20 @@ public func peerAllowedReactionListController(
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
controller.willDisappear = { _ in
|
||||
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.AllowedReactions(id: peerId))
|
||||
|> deliverOnMainQueue).start(next: { initialAllowedReactions in
|
||||
let _ = (combineLatest(
|
||||
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 })
|
||||
guard let updatedMode = state.updatedMode, let updatedAllowedReactions = state.updatedAllowedReactions else {
|
||||
return
|
||||
@ -458,7 +527,15 @@ public func peerAllowedReactionListController(
|
||||
case .all:
|
||||
updatedValue = .all
|
||||
case .some:
|
||||
updatedValue = .limited(Array(updatedAllowedReactions))
|
||||
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))
|
||||
}
|
||||
} else {
|
||||
updatedValue = .limited(Array(updatedAllowedReactions))
|
||||
}
|
||||
case .empty:
|
||||
updatedValue = .empty
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ class EmojiHeaderComponent: Component {
|
||||
let animationCache: AnimationCache
|
||||
let animationRenderer: MultiAnimationRenderer
|
||||
let placeholderColor: UIColor
|
||||
let accentColor: UIColor
|
||||
let fileId: Int64
|
||||
let isVisible: Bool
|
||||
let hasIdleAnimations: Bool
|
||||
@ -30,6 +31,7 @@ class EmojiHeaderComponent: Component {
|
||||
animationCache: AnimationCache,
|
||||
animationRenderer: MultiAnimationRenderer,
|
||||
placeholderColor: UIColor,
|
||||
accentColor: UIColor,
|
||||
fileId: Int64,
|
||||
isVisible: Bool,
|
||||
hasIdleAnimations: Bool
|
||||
@ -38,13 +40,14 @@ class EmojiHeaderComponent: Component {
|
||||
self.animationCache = animationCache
|
||||
self.animationRenderer = animationRenderer
|
||||
self.placeholderColor = placeholderColor
|
||||
self.accentColor = accentColor
|
||||
self.fileId = fileId
|
||||
self.isVisible = isVisible
|
||||
self.hasIdleAnimations = hasIdleAnimations
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -126,8 +129,11 @@ class EmojiHeaderComponent: Component {
|
||||
content: .animation(
|
||||
content: .customEmoji(fileId: component.fileId),
|
||||
size: CGSize(width: 100.0, height: 100.0),
|
||||
placeholderColor: component.placeholderColor
|
||||
placeholderColor: component.placeholderColor,
|
||||
themeColor: component.accentColor,
|
||||
loopMode: .forever
|
||||
),
|
||||
isVisibleForAnimations: true,
|
||||
action: nil,
|
||||
longTapAction: nil
|
||||
)),
|
||||
|
@ -1985,6 +1985,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
animationCache: state.animationCache,
|
||||
animationRenderer: state.animationRenderer,
|
||||
placeholderColor: environment.theme.list.mediaPlaceholderColor,
|
||||
accentColor: environment.theme.list.itemAccentColor,
|
||||
fileId: fileId,
|
||||
isVisible: starIsVisible,
|
||||
hasIdleAnimations: state.hasIdleAnimations
|
||||
|
@ -406,7 +406,7 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
targetContainerNode.view.superview?.bringSubviewToFront(targetContainerNode.view)
|
||||
|
||||
let standaloneReactionAnimation = StandaloneReactionAnimation(useDirectRendering: true)
|
||||
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: nil, useDirectRendering: true)
|
||||
self.standaloneReactionAnimation = standaloneReactionAnimation
|
||||
|
||||
targetContainerNode.addSubnode(standaloneReactionAnimation)
|
||||
|
@ -32,6 +32,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
||||
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/GZip:GZip",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -20,6 +20,7 @@ import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import EmojiTextAttachmentView
|
||||
import TextFormat
|
||||
import GZip
|
||||
|
||||
public final class ReactionItem {
|
||||
public struct Reaction: Equatable {
|
||||
@ -106,7 +107,7 @@ private final class ExpandItemView: UIView {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -199,6 +200,45 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
private var hasPremium: Bool?
|
||||
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) {
|
||||
self.context = context
|
||||
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 {
|
||||
self.emojiContentDisposable?.dispose()
|
||||
self.availableReactionsDisposable?.dispose()
|
||||
self.hasPremiumDisposable?.dispose()
|
||||
self.genericReactionEffectDisposable?.dispose()
|
||||
}
|
||||
|
||||
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.rightBackgroundMaskNode, frame: CGRect(x: currentMaskFrame.maxX, y: 0.0, width: 1000.0, height: self.currentContentHeight + self.extensionDistance))
|
||||
} 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)
|
||||
}
|
||||
|
||||
@ -1175,16 +1221,22 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
//targetSnapshotView.layer.sublayers![0].backgroundColor = UIColor.green.cgColor
|
||||
|
||||
let sourceFrame = itemNode.view.convert(itemNode.bounds, to: self.view)
|
||||
|
||||
var selfTargetBounds = targetView.bounds
|
||||
if case .builtin = itemNode.item.reaction.rawValue {
|
||||
selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5)
|
||||
if let targetView = targetView as? ReactionIconView, let iconFrame = targetView.iconFrame, !"".isEmpty {
|
||||
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)
|
||||
|
||||
targetSnapshotView.frame = targetFrame
|
||||
//targetSnapshotView.backgroundColor = .blue
|
||||
self.view.insertSubview(targetSnapshotView, belowSubview: itemNode.view)
|
||||
|
||||
var completedTarget = false
|
||||
@ -1199,6 +1251,14 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
let duration: Double = 0.16
|
||||
|
||||
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)
|
||||
targetSnapshotView.alpha = 1.0
|
||||
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 {
|
||||
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))
|
||||
view.animationSpeed = 1.0
|
||||
view.backgroundColor = nil
|
||||
@ -1489,7 +1562,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
if self.didTriggerExpandedReaction {
|
||||
self.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: hideNode, completion: { [weak self] in
|
||||
if let strongSelf = self, strongSelf.didTriggerExpandedReaction, let addStandaloneReactionAnimation = addStandaloneReactionAnimation {
|
||||
let standaloneReactionAnimation = StandaloneReactionAnimation()
|
||||
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: strongSelf.genericReactionEffect)
|
||||
|
||||
addStandaloneReactionAnimation(standaloneReactionAnimation)
|
||||
|
||||
@ -1775,6 +1848,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
public final class StandaloneReactionAnimation: ASDisplayNode {
|
||||
private let genericReactionEffect: String?
|
||||
private let useDirectRendering: Bool
|
||||
private var itemNode: ReactionNode? = nil
|
||||
private var itemNodeIsEmbedded: Bool = false
|
||||
@ -1783,7 +1857,8 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
||||
|
||||
private weak var targetView: UIView?
|
||||
|
||||
public init(useDirectRendering: Bool = false) {
|
||||
public init(genericReactionEffect: String?, useDirectRendering: Bool = false) {
|
||||
self.genericReactionEffect = genericReactionEffect
|
||||
self.useDirectRendering = useDirectRendering
|
||||
|
||||
super.init()
|
||||
@ -1922,7 +1997,16 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
||||
} else if itemNode.item.isCustom {
|
||||
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))
|
||||
view.animationSpeed = 1.0
|
||||
view.backgroundColor = nil
|
||||
@ -2043,9 +2127,10 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
||||
intermediateCompletion()
|
||||
} else {
|
||||
if isLarge {
|
||||
let genericReactionEffect = strongSelf.genericReactionEffect
|
||||
strongSelf.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: true, completion: {
|
||||
if let addStandaloneReactionAnimation = addStandaloneReactionAnimation {
|
||||
let standaloneReactionAnimation = StandaloneReactionAnimation()
|
||||
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: genericReactionEffect)
|
||||
|
||||
addStandaloneReactionAnimation(standaloneReactionAnimation)
|
||||
|
||||
|
@ -291,7 +291,7 @@ public class ItemListReactionItemNode: ListViewItemNode, ItemListItemNode {
|
||||
context: item.context,
|
||||
animationCache: item.context.animationCache,
|
||||
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: {},
|
||||
containerSize: iconBoundingSize
|
||||
|
@ -92,6 +92,9 @@ class ReactionChatPreviewItemNode: ListViewItemNode {
|
||||
|
||||
private var animationCache: AnimationCache?
|
||||
|
||||
private var genericReactionEffect: String?
|
||||
private var genericReactionEffectDisposable: Disposable?
|
||||
|
||||
init() {
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
@ -111,6 +114,10 @@ class ReactionChatPreviewItemNode: ListViewItemNode {
|
||||
self.addSubnode(self.containerNode)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.genericReactionEffectDisposable?.dispose()
|
||||
}
|
||||
|
||||
override func 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) {
|
||||
if let item = self.item, let updatedReaction = item.reaction, let messageNode = self.messageNode as? ChatMessageItemNodeProtocol {
|
||||
if let targetView = messageNode.targetReactionView(value: updatedReaction) {
|
||||
@ -211,7 +228,8 @@ class ReactionChatPreviewItemNode: ListViewItemNode {
|
||||
}
|
||||
|
||||
if let supernode = self.supernode {
|
||||
let standaloneReactionAnimation = StandaloneReactionAnimation()
|
||||
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: self.genericReactionEffect)
|
||||
self.loadNextGenericReactionEffect(context: item.context)
|
||||
self.standaloneReactionAnimation = standaloneReactionAnimation
|
||||
|
||||
let animationCache = item.context.animationCache
|
||||
@ -309,6 +327,10 @@ class ReactionChatPreviewItemNode: ListViewItemNode {
|
||||
|
||||
strongSelf.item = item
|
||||
|
||||
if strongSelf.genericReactionEffectDisposable == nil {
|
||||
strongSelf.loadNextGenericReactionEffect(context: item.context)
|
||||
}
|
||||
|
||||
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
|
||||
|
||||
var topOffset: CGFloat = 16.0
|
||||
|
@ -362,7 +362,8 @@ final class StickerPackEmojisItemNode: GridItemNode {
|
||||
content: .animation(animationData),
|
||||
itemFile: item.file,
|
||||
subgroupId: nil,
|
||||
icon: .none
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
),
|
||||
context: context,
|
||||
attemptSynchronousLoad: attemptSynchronousLoads,
|
||||
|
@ -354,6 +354,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[42402760] = { return Api.InputStickerSet.parse_inputStickerSetAnimatedEmoji($0) }
|
||||
dict[215889721] = { return Api.InputStickerSet.parse_inputStickerSetAnimatedEmojiAnimations($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[-1645763991] = { return Api.InputStickerSet.parse_inputStickerSetID($0) }
|
||||
dict[-930399486] = { return Api.InputStickerSet.parse_inputStickerSetPremiumGifts($0) }
|
||||
|
@ -677,6 +677,8 @@ public extension Api {
|
||||
case inputStickerSetAnimatedEmoji
|
||||
case inputStickerSetAnimatedEmojiAnimations
|
||||
case inputStickerSetDice(emoticon: String)
|
||||
case inputStickerSetEmojiDefaultStatuses
|
||||
case inputStickerSetEmojiGenericAnimations
|
||||
case inputStickerSetEmpty
|
||||
case inputStickerSetID(id: Int64, accessHash: Int64)
|
||||
case inputStickerSetPremiumGifts
|
||||
@ -701,6 +703,18 @@ public extension Api {
|
||||
buffer.appendInt32(-427863538)
|
||||
}
|
||||
serializeString(emoticon, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .inputStickerSetEmojiDefaultStatuses:
|
||||
if boxed {
|
||||
buffer.appendInt32(701560302)
|
||||
}
|
||||
|
||||
break
|
||||
case .inputStickerSetEmojiGenericAnimations:
|
||||
if boxed {
|
||||
buffer.appendInt32(80008398)
|
||||
}
|
||||
|
||||
break
|
||||
case .inputStickerSetEmpty:
|
||||
if boxed {
|
||||
@ -738,6 +752,10 @@ public extension Api {
|
||||
return ("inputStickerSetAnimatedEmojiAnimations", [])
|
||||
case .inputStickerSetDice(let emoticon):
|
||||
return ("inputStickerSetDice", [("emoticon", String(describing: emoticon))])
|
||||
case .inputStickerSetEmojiDefaultStatuses:
|
||||
return ("inputStickerSetEmojiDefaultStatuses", [])
|
||||
case .inputStickerSetEmojiGenericAnimations:
|
||||
return ("inputStickerSetEmojiGenericAnimations", [])
|
||||
case .inputStickerSetEmpty:
|
||||
return ("inputStickerSetEmpty", [])
|
||||
case .inputStickerSetID(let id, let accessHash):
|
||||
@ -766,6 +784,12 @@ public extension Api {
|
||||
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? {
|
||||
return Api.InputStickerSet.inputStickerSetEmpty
|
||||
}
|
||||
|
@ -841,7 +841,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
||||
} else if item.peer.isFake {
|
||||
credibilityIcon = .fake(color: item.presentationData.theme.chat.message.incoming.scamColor)
|
||||
} 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 {
|
||||
credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||
} else if item.peer.isPremium && !premiumConfiguration.isPremiumDisabled {
|
||||
@ -1034,6 +1034,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
||||
animationCache: animationCache,
|
||||
animationRenderer: animationRenderer,
|
||||
content: credibilityIcon,
|
||||
isVisibleForAnimations: true,
|
||||
action: nil,
|
||||
longTapAction: nil,
|
||||
emojiFileUpdated: nil
|
||||
|
@ -1171,14 +1171,16 @@ public class Account {
|
||||
if !self.supplementary {
|
||||
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(managedGenericEmojiEffects(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(managedAllPremiumStickers(postbox: self.postbox, network: self.network).start())
|
||||
self.managedOperationsDisposable.add(managedRecentStatusEmoji(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(managedTopReactions(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(managedAllPremiumStickers(postbox: self.postbox, network: self.network).start())
|
||||
self.managedOperationsDisposable.add(managedRecentStatusEmoji(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(managedTopReactions(postbox: self.postbox, network: self.network).start())
|
||||
|
||||
if !supplementary {
|
||||
let mediaBox = postbox.mediaBox
|
||||
|
@ -50,20 +50,24 @@ public extension TelegramMediaFile {
|
||||
extension StickerPackReference {
|
||||
init?(apiInputSet: Api.InputStickerSet) {
|
||||
switch apiInputSet {
|
||||
case .inputStickerSetEmpty:
|
||||
return nil
|
||||
case let .inputStickerSetID(id, accessHash):
|
||||
self = .id(id: id, accessHash: accessHash)
|
||||
case let .inputStickerSetShortName(shortName):
|
||||
self = .name(shortName)
|
||||
case .inputStickerSetAnimatedEmoji:
|
||||
self = .animatedEmoji
|
||||
case let .inputStickerSetDice(emoticon):
|
||||
self = .dice(emoticon)
|
||||
case .inputStickerSetAnimatedEmojiAnimations:
|
||||
self = .animatedEmojiAnimations
|
||||
case .inputStickerSetPremiumGifts:
|
||||
self = .premiumGifts
|
||||
case .inputStickerSetEmpty:
|
||||
return nil
|
||||
case let .inputStickerSetID(id, accessHash):
|
||||
self = .id(id: id, accessHash: accessHash)
|
||||
case let .inputStickerSetShortName(shortName):
|
||||
self = .name(shortName)
|
||||
case .inputStickerSetAnimatedEmoji:
|
||||
self = .animatedEmoji
|
||||
case let .inputStickerSetDice(emoticon):
|
||||
self = .dice(emoticon)
|
||||
case .inputStickerSetAnimatedEmojiAnimations:
|
||||
self = .animatedEmojiAnimations
|
||||
case .inputStickerSetPremiumGifts:
|
||||
self = .premiumGifts
|
||||
case .inputStickerSetEmojiGenericAnimations:
|
||||
self = .emojiGenericAnimations
|
||||
case .inputStickerSetEmojiDefaultStatuses:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -43,26 +43,26 @@ public func addSavedSticker(postbox: Postbox, network: Network, file: TelegramMe
|
||||
if case let .Sticker(_, maybePackReference, _) = attribute, let packReference = maybePackReference {
|
||||
var fetchReference: StickerPackReference?
|
||||
switch packReference {
|
||||
case .name:
|
||||
case .name:
|
||||
fetchReference = packReference
|
||||
case let .id(id, _):
|
||||
let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: Namespaces.ItemCollection.CloudStickerPacks, id: id))
|
||||
var found = false
|
||||
inner: for item in items {
|
||||
if let stickerItem = item as? StickerPackItem {
|
||||
if stickerItem.file.fileId == file.fileId {
|
||||
let stringRepresentations = stickerItem.getStringRepresentationsOfIndexKeys()
|
||||
found = true
|
||||
addSavedSticker(transaction: transaction, file: stickerItem.file, stringRepresentations: stringRepresentations)
|
||||
break inner
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
fetchReference = packReference
|
||||
case let .id(id, _):
|
||||
let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: Namespaces.ItemCollection.CloudStickerPacks, id: id))
|
||||
var found = false
|
||||
inner: for item in items {
|
||||
if let stickerItem = item as? StickerPackItem {
|
||||
if stickerItem.file.fileId == file.fileId {
|
||||
let stringRepresentations = stickerItem.getStringRepresentationsOfIndexKeys()
|
||||
found = true
|
||||
addSavedSticker(transaction: transaction, file: stickerItem.file, stringRepresentations: stringRepresentations)
|
||||
break inner
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
fetchReference = packReference
|
||||
}
|
||||
case .animatedEmoji, .animatedEmojiAnimations, .dice, .premiumGifts:
|
||||
break
|
||||
}
|
||||
case .animatedEmoji, .animatedEmojiAnimations, .dice, .premiumGifts, .emojiGenericAnimations:
|
||||
break
|
||||
}
|
||||
if let fetchReference = fetchReference {
|
||||
return network.request(Api.functions.messages.getStickerSet(stickerset: fetchReference.apiInputStickerSet, hash: 0))
|
||||
|
@ -48,6 +48,7 @@ public struct Namespaces {
|
||||
public static let CloudAnimatedEmojiReactions: Int32 = 6
|
||||
public static let CloudPremiumGifts: Int32 = 7
|
||||
public static let CloudEmojiPacks: Int32 = 8
|
||||
public static let CloudEmojiGenericAnimations: Int32 = 9
|
||||
}
|
||||
|
||||
public struct OrderedItemList {
|
||||
|
@ -20,6 +20,7 @@ public enum StickerPackReference: PostboxCoding, Hashable, Equatable, Codable {
|
||||
case dice(String)
|
||||
case animatedEmojiAnimations
|
||||
case premiumGifts
|
||||
case emojiGenericAnimations
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
switch decoder.decodeInt32ForKey("r", orElse: 0) {
|
||||
@ -66,22 +67,24 @@ public enum StickerPackReference: PostboxCoding, Hashable, Equatable, Codable {
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
switch self {
|
||||
case let .id(id, accessHash):
|
||||
encoder.encodeInt32(0, forKey: "r")
|
||||
encoder.encodeInt64(id, forKey: "i")
|
||||
encoder.encodeInt64(accessHash, forKey: "h")
|
||||
case let .name(name):
|
||||
encoder.encodeInt32(1, forKey: "r")
|
||||
encoder.encodeString(name, forKey: "n")
|
||||
case .animatedEmoji:
|
||||
encoder.encodeInt32(2, forKey: "r")
|
||||
case let .dice(emoji):
|
||||
encoder.encodeInt32(3, forKey: "r")
|
||||
encoder.encodeString(emoji, forKey: "e")
|
||||
case .animatedEmojiAnimations:
|
||||
encoder.encodeInt32(4, forKey: "r")
|
||||
case .premiumGifts:
|
||||
encoder.encodeInt32(5, forKey: "r")
|
||||
case let .id(id, accessHash):
|
||||
encoder.encodeInt32(0, forKey: "r")
|
||||
encoder.encodeInt64(id, forKey: "i")
|
||||
encoder.encodeInt64(accessHash, forKey: "h")
|
||||
case let .name(name):
|
||||
encoder.encodeInt32(1, forKey: "r")
|
||||
encoder.encodeString(name, forKey: "n")
|
||||
case .animatedEmoji:
|
||||
encoder.encodeInt32(2, forKey: "r")
|
||||
case let .dice(emoji):
|
||||
encoder.encodeInt32(3, forKey: "r")
|
||||
encoder.encodeString(emoji, forKey: "e")
|
||||
case .animatedEmojiAnimations:
|
||||
encoder.encodeInt32(4, forKey: "r")
|
||||
case .premiumGifts:
|
||||
encoder.encodeInt32(5, forKey: "r")
|
||||
case .emojiGenericAnimations:
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,47 +108,55 @@ public enum StickerPackReference: PostboxCoding, Hashable, Equatable, Codable {
|
||||
try container.encode(4 as Int32, forKey: "r")
|
||||
case .premiumGifts:
|
||||
try container.encode(5 as Int32, forKey: "r")
|
||||
case .emojiGenericAnimations:
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
public static func ==(lhs: StickerPackReference, rhs: StickerPackReference) -> Bool {
|
||||
switch lhs {
|
||||
case let .id(id, accessHash):
|
||||
if case .id(id, accessHash) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .name(name):
|
||||
if case .name(name) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .animatedEmoji:
|
||||
if case .animatedEmoji = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .dice(emoji):
|
||||
if case .dice(emoji) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .animatedEmojiAnimations:
|
||||
if case .animatedEmojiAnimations = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .premiumGifts:
|
||||
if case .premiumGifts = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .id(id, accessHash):
|
||||
if case .id(id, accessHash) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .name(name):
|
||||
if case .name(name) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .animatedEmoji:
|
||||
if case .animatedEmoji = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .dice(emoji):
|
||||
if case .dice(emoji) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .animatedEmojiAnimations:
|
||||
if case .animatedEmojiAnimations = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .premiumGifts:
|
||||
if case .premiumGifts = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .emojiGenericAnimations:
|
||||
if case .emojiGenericAnimations = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -138,6 +138,20 @@ func _internal_cachedStickerPack(postbox: Postbox, network: Network, reference:
|
||||
} else {
|
||||
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
|
||||
@ -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 {
|
||||
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
|
||||
}
|
||||
|
@ -10,18 +10,20 @@ extension StickerPackReference {
|
||||
|
||||
var apiInputStickerSet: Api.InputStickerSet {
|
||||
switch self {
|
||||
case let .id(id, accessHash):
|
||||
return .inputStickerSetID(id: id, accessHash: accessHash)
|
||||
case let .name(name):
|
||||
return .inputStickerSetShortName(shortName: name)
|
||||
case .animatedEmoji:
|
||||
return .inputStickerSetAnimatedEmoji
|
||||
case let .dice(emoji):
|
||||
return .inputStickerSetDice(emoticon: emoji)
|
||||
case .animatedEmojiAnimations:
|
||||
return .inputStickerSetAnimatedEmojiAnimations
|
||||
case .premiumGifts:
|
||||
return .inputStickerSetPremiumGifts
|
||||
case let .id(id, accessHash):
|
||||
return .inputStickerSetID(id: id, accessHash: accessHash)
|
||||
case let .name(name):
|
||||
return .inputStickerSetShortName(shortName: name)
|
||||
case .animatedEmoji:
|
||||
return .inputStickerSetAnimatedEmoji
|
||||
case let .dice(emoji):
|
||||
return .inputStickerSetDice(emoticon: emoji)
|
||||
case .animatedEmojiAnimations:
|
||||
return .inputStickerSetAnimatedEmojiAnimations
|
||||
case .premiumGifts:
|
||||
return .inputStickerSetPremiumGifts
|
||||
case .emojiGenericAnimations:
|
||||
return .inputStickerSetEmojiGenericAnimations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,24 +28,27 @@ func _internal_requestStickerSet(postbox: Postbox, network: Network, reference:
|
||||
let input: Api.InputStickerSet
|
||||
|
||||
switch reference {
|
||||
case let .name(name):
|
||||
collectionId = nil
|
||||
input = .inputStickerSetShortName(shortName: name)
|
||||
case let .id(id, accessHash):
|
||||
collectionId = ItemCollectionId(namespace: Namespaces.ItemCollection.CloudStickerPacks, id: id)
|
||||
input = .inputStickerSetID(id: id, accessHash: accessHash)
|
||||
case .animatedEmoji:
|
||||
collectionId = nil
|
||||
input = .inputStickerSetAnimatedEmoji
|
||||
case let .dice(emoji):
|
||||
collectionId = nil
|
||||
input = .inputStickerSetDice(emoticon: emoji)
|
||||
case .animatedEmojiAnimations:
|
||||
collectionId = nil
|
||||
input = .inputStickerSetAnimatedEmojiAnimations
|
||||
case .premiumGifts:
|
||||
collectionId = nil
|
||||
input = .inputStickerSetPremiumGifts
|
||||
case let .name(name):
|
||||
collectionId = nil
|
||||
input = .inputStickerSetShortName(shortName: name)
|
||||
case let .id(id, accessHash):
|
||||
collectionId = ItemCollectionId(namespace: Namespaces.ItemCollection.CloudStickerPacks, id: id)
|
||||
input = .inputStickerSetID(id: id, accessHash: accessHash)
|
||||
case .animatedEmoji:
|
||||
collectionId = nil
|
||||
input = .inputStickerSetAnimatedEmoji
|
||||
case let .dice(emoji):
|
||||
collectionId = nil
|
||||
input = .inputStickerSetDice(emoticon: emoji)
|
||||
case .animatedEmojiAnimations:
|
||||
collectionId = nil
|
||||
input = .inputStickerSetAnimatedEmojiAnimations
|
||||
case .premiumGifts:
|
||||
collectionId = nil
|
||||
input = .inputStickerSetPremiumGifts
|
||||
case .emojiGenericAnimations:
|
||||
collectionId = nil
|
||||
input = .inputStickerSetEmojiGenericAnimations
|
||||
}
|
||||
|
||||
let localSignal: (ItemCollectionId) -> Signal<(ItemCollectionInfo, [ItemCollectionItem])?, NoError> = { collectionId in
|
||||
|
@ -610,6 +610,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati
|
||||
panelHighlightedIconBackgroundColor: UIColor(rgb: 0x808080).withMultipliedAlpha(0.25),
|
||||
panelHighlightedIconColor: UIColor(rgb: 0x808080).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.35),
|
||||
panelContentVibrantOverlayColor: UIColor(rgb: 0x808080),
|
||||
panelContentControlVibrantOverlayColor: UIColor(rgb: 0x808080).mixedWith(UIColor(rgb: 0x000000), alpha: 0.35),
|
||||
stickersBackgroundColor: UIColor(rgb: 0x000000),
|
||||
stickersSectionTextColor: UIColor(rgb: 0x7b7b7b),
|
||||
stickersSearchBackgroundColor: UIColor(rgb: 0x1c1c1d),
|
||||
|
@ -442,6 +442,7 @@ public func customizeDefaultDarkTintedPresentationTheme(theme: PresentationTheme
|
||||
panelHighlightedIconBackgroundColor: mainSecondaryTextColor?.withAlphaComponent(0.5).withMultipliedAlpha(0.25),
|
||||
panelHighlightedIconColor: mainSecondaryTextColor?.withAlphaComponent(0.5).mixedWith(chat.inputPanel.primaryTextColor, alpha: 0.35),
|
||||
panelContentVibrantOverlayColor: mainSecondaryTextColor?.withAlphaComponent(0.5),
|
||||
panelContentControlVibrantOverlayColor: mainSecondaryTextColor?.withAlphaComponent(0.3),
|
||||
stickersBackgroundColor: additionalBackgroundColor,
|
||||
stickersSectionTextColor: mainSecondaryTextColor?.withAlphaComponent(0.5),
|
||||
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),
|
||||
panelHighlightedIconColor: mainSecondaryTextColor.withAlphaComponent(0.5).mixedWith(inputPanel.primaryTextColor, alpha: 0.35),
|
||||
panelContentVibrantOverlayColor: mainSecondaryTextColor.withAlphaComponent(0.5),
|
||||
panelContentControlVibrantOverlayColor: mainSecondaryTextColor.withAlphaComponent(0.3),
|
||||
stickersBackgroundColor: additionalBackgroundColor,
|
||||
stickersSectionTextColor: mainSecondaryTextColor.withAlphaComponent(0.5),
|
||||
stickersSearchBackgroundColor: accentColor.withMultiplied(hue: 1.009, saturation: 0.621, brightness: 0.15),
|
||||
|
@ -863,7 +863,8 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
panelIconColor: UIColor(rgb: 0x858e99),
|
||||
panelHighlightedIconBackgroundColor: UIColor(rgb: 0x858e99, alpha: 0.2),
|
||||
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),
|
||||
stickersSectionTextColor: UIColor(rgb: 0x9099a2),
|
||||
stickersSearchBackgroundColor: UIColor(rgb: 0xd9dbe1),
|
||||
|
@ -1144,6 +1144,7 @@ public final class PresentationThemeInputMediaPanel {
|
||||
public let panelHighlightedIconBackgroundColor: UIColor
|
||||
public let panelHighlightedIconColor: UIColor
|
||||
public let panelContentVibrantOverlayColor: UIColor
|
||||
public let panelContentControlVibrantOverlayColor: UIColor
|
||||
public let stickersBackgroundColor: UIColor
|
||||
public let stickersSectionTextColor: UIColor
|
||||
public let stickersSearchBackgroundColor: UIColor
|
||||
@ -1153,12 +1154,28 @@ public final class PresentationThemeInputMediaPanel {
|
||||
public let gifsBackgroundColor: 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.panelIconColor = panelIconColor
|
||||
self.panelHighlightedIconBackgroundColor = panelHighlightedIconBackgroundColor
|
||||
self.panelHighlightedIconColor = panelHighlightedIconColor
|
||||
self.panelContentVibrantOverlayColor = panelContentVibrantOverlayColor
|
||||
self.panelContentControlVibrantOverlayColor = panelContentControlVibrantOverlayColor
|
||||
self.stickersBackgroundColor = stickersBackgroundColor
|
||||
self.stickersSectionTextColor = stickersSectionTextColor
|
||||
self.stickersSearchBackgroundColor = stickersSearchBackgroundColor
|
||||
@ -1169,8 +1186,37 @@ public final class PresentationThemeInputMediaPanel {
|
||||
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 {
|
||||
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)
|
||||
public func withUpdated(
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1607,6 +1607,7 @@ extension PresentationThemeInputMediaPanel: Codable {
|
||||
case panelHighlightedIconBg
|
||||
case panelHighlightedIcon
|
||||
case panelContentVibrantOverlay
|
||||
case panelContentControlVibrantOverlay
|
||||
case stickersBg
|
||||
case stickersSectionText
|
||||
case stickersSearchBg
|
||||
@ -1644,6 +1645,7 @@ extension PresentationThemeInputMediaPanel: Codable {
|
||||
panelHighlightedIconBackgroundColor: try decodeColor(values, .panelHighlightedIconBg),
|
||||
panelHighlightedIconColor: panelHighlightedIconColor,
|
||||
panelContentVibrantOverlayColor: try decodeColor(values, .panelContentVibrantOverlay, fallbackKey: "\(codingPath).stickersSectionText"),
|
||||
panelContentControlVibrantOverlayColor: try decodeColor(values, .panelContentControlVibrantOverlay, fallbackKey: "\(codingPath).stickersSectionText"),
|
||||
stickersBackgroundColor: try decodeColor(values, .stickersBg),
|
||||
stickersSectionTextColor: try decodeColor(values, .stickersSectionText),
|
||||
stickersSearchBackgroundColor: try decodeColor(values, .stickersSearchBg),
|
||||
@ -1660,6 +1662,7 @@ extension PresentationThemeInputMediaPanel: Codable {
|
||||
try encodeColor(&values, self.panelHighlightedIconBackgroundColor, .panelHighlightedIconBg)
|
||||
try encodeColor(&values, self.panelHighlightedIconColor, .panelHighlightedIcon)
|
||||
try encodeColor(&values, self.panelContentVibrantOverlayColor, .panelContentVibrantOverlay)
|
||||
try encodeColor(&values, self.panelContentControlVibrantOverlayColor, .panelContentControlVibrantOverlay)
|
||||
try encodeColor(&values, self.stickersBackgroundColor, .stickersBg)
|
||||
try encodeColor(&values, self.stickersSectionTextColor, .stickersSectionText)
|
||||
try encodeColor(&values, self.stickersSearchBackgroundColor, .stickersSearchBg)
|
||||
|
@ -54,17 +54,27 @@ public final class AnimationCacheItem {
|
||||
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
|
||||
private let advanceImpl: (Advance, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame?
|
||||
private let advanceImpl: (Advance, AnimationCacheItemFrame.RequestedFormat) -> AdvanceResult?
|
||||
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.advanceImpl = advanceImpl
|
||||
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)
|
||||
}
|
||||
|
||||
@ -669,12 +679,14 @@ private final class AnimationCacheItemAccessor {
|
||||
self.currentDctData = dctData
|
||||
}
|
||||
|
||||
private func loadNextFrame() {
|
||||
private func loadNextFrame() -> Bool {
|
||||
var didLoop = false
|
||||
let index: Int
|
||||
if let currentFrame = self.currentFrame {
|
||||
if currentFrame.index + 1 >= self.durationMapping.count {
|
||||
index = 0
|
||||
self.compressedDataReader = nil
|
||||
didLoop = true
|
||||
} else {
|
||||
index = currentFrame.index + 1
|
||||
}
|
||||
@ -689,7 +701,7 @@ private final class AnimationCacheItemAccessor {
|
||||
|
||||
guard let compressedDataReader = self.compressedDataReader else {
|
||||
self.currentFrame = nil
|
||||
return
|
||||
return didLoop
|
||||
}
|
||||
|
||||
do {
|
||||
@ -779,17 +791,22 @@ private final class AnimationCacheItemAccessor {
|
||||
self.currentFrame = nil
|
||||
self.compressedDataReader = nil
|
||||
}
|
||||
|
||||
return didLoop
|
||||
}
|
||||
|
||||
func reset() {
|
||||
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 {
|
||||
case let .frames(count):
|
||||
for _ in 0 ..< count {
|
||||
self.loadNextFrame()
|
||||
if self.loadNextFrame() {
|
||||
didLoop = true
|
||||
}
|
||||
}
|
||||
case let .duration(duration):
|
||||
var durationOverflow = duration
|
||||
@ -798,12 +815,16 @@ private final class AnimationCacheItemAccessor {
|
||||
currentFrame.remainingDuration -= durationOverflow
|
||||
if currentFrame.remainingDuration <= 0.0 {
|
||||
durationOverflow = -currentFrame.remainingDuration
|
||||
self.loadNextFrame()
|
||||
if self.loadNextFrame() {
|
||||
didLoop = true
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
self.loadNextFrame()
|
||||
if self.loadNextFrame() {
|
||||
didLoop = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -818,36 +839,42 @@ private final class AnimationCacheItemAccessor {
|
||||
let currentSurface = ImageARGB(width: currentFrame.yuva.yPlane.width, height: currentFrame.yuva.yPlane.height, rowAlignment: 32)
|
||||
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:
|
||||
return AnimationCacheItemFrame(
|
||||
format: .yuva(
|
||||
y: AnimationCacheItemFrame.Plane(
|
||||
data: currentFrame.yuva.yPlane.data,
|
||||
width: currentFrame.yuva.yPlane.width,
|
||||
height: currentFrame.yuva.yPlane.height,
|
||||
bytesPerRow: currentFrame.yuva.yPlane.bytesPerRow
|
||||
return AnimationCacheItem.AdvanceResult(
|
||||
frame: AnimationCacheItemFrame(
|
||||
format: .yuva(
|
||||
y: AnimationCacheItemFrame.Plane(
|
||||
data: currentFrame.yuva.yPlane.data,
|
||||
width: currentFrame.yuva.yPlane.width,
|
||||
height: currentFrame.yuva.yPlane.height,
|
||||
bytesPerRow: currentFrame.yuva.yPlane.bytesPerRow
|
||||
),
|
||||
u: AnimationCacheItemFrame.Plane(
|
||||
data: currentFrame.yuva.uPlane.data,
|
||||
width: currentFrame.yuva.uPlane.width,
|
||||
height: currentFrame.yuva.uPlane.height,
|
||||
bytesPerRow: currentFrame.yuva.uPlane.bytesPerRow
|
||||
),
|
||||
v: AnimationCacheItemFrame.Plane(
|
||||
data: currentFrame.yuva.vPlane.data,
|
||||
width: currentFrame.yuva.vPlane.width,
|
||||
height: currentFrame.yuva.vPlane.height,
|
||||
bytesPerRow: currentFrame.yuva.vPlane.bytesPerRow
|
||||
),
|
||||
a: AnimationCacheItemFrame.Plane(
|
||||
data: currentFrame.yuva.aPlane.data,
|
||||
width: currentFrame.yuva.aPlane.width,
|
||||
height: currentFrame.yuva.aPlane.height,
|
||||
bytesPerRow: currentFrame.yuva.aPlane.bytesPerRow
|
||||
)
|
||||
),
|
||||
u: AnimationCacheItemFrame.Plane(
|
||||
data: currentFrame.yuva.uPlane.data,
|
||||
width: currentFrame.yuva.uPlane.width,
|
||||
height: currentFrame.yuva.uPlane.height,
|
||||
bytesPerRow: currentFrame.yuva.uPlane.bytesPerRow
|
||||
),
|
||||
v: AnimationCacheItemFrame.Plane(
|
||||
data: currentFrame.yuva.vPlane.data,
|
||||
width: currentFrame.yuva.vPlane.width,
|
||||
height: currentFrame.yuva.vPlane.height,
|
||||
bytesPerRow: currentFrame.yuva.vPlane.bytesPerRow
|
||||
),
|
||||
a: AnimationCacheItemFrame.Plane(
|
||||
data: currentFrame.yuva.aPlane.data,
|
||||
width: currentFrame.yuva.aPlane.width,
|
||||
height: currentFrame.yuva.aPlane.height,
|
||||
bytesPerRow: currentFrame.yuva.aPlane.bytesPerRow
|
||||
)
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
switch frame.format {
|
||||
switch frame.frame.format {
|
||||
case .rgba:
|
||||
return nil
|
||||
case let .yuva(y, u, v, a):
|
||||
@ -1245,7 +1272,7 @@ private func adaptItemFromHigherResolution(currentQueue: Queue, itemPath: String
|
||||
yuva.aPlane.copyScaled(fromPlane: a)
|
||||
}
|
||||
|
||||
return frame.duration
|
||||
return frame.frame.duration
|
||||
}, 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 {
|
||||
return false
|
||||
}
|
||||
switch frame.format {
|
||||
switch frame.frame.format {
|
||||
case .rgba:
|
||||
return false
|
||||
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.aPlane.copyScaled(fromPlane: a)
|
||||
|
||||
return frame.duration
|
||||
return frame.frame.duration
|
||||
}, proposedWidth: y.width, proposedHeight: y.height, insertKeyframe: true)
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ swift_library(
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
|
||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
||||
@ -21,6 +22,8 @@ swift_library(
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/lottie-ios:Lottie",
|
||||
"//submodules/GZip:GZip",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -11,6 +11,9 @@ import Postbox
|
||||
import EmojiTextAttachmentView
|
||||
import AppBundle
|
||||
import TextFormat
|
||||
import Lottie
|
||||
import GZip
|
||||
import HierarchyTrackingLayer
|
||||
|
||||
public final class EmojiStatusComponent: Component {
|
||||
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 {
|
||||
case none
|
||||
case premium(color: UIColor)
|
||||
case verified(fillColor: UIColor, foregroundColor: UIColor)
|
||||
case fake(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 animationCache: AnimationCache
|
||||
public let animationRenderer: MultiAnimationRenderer
|
||||
public let content: Content
|
||||
public let isVisibleForAnimations: Bool
|
||||
public let action: (() -> Void)?
|
||||
public let longTapAction: (() -> Void)?
|
||||
public let emojiFileUpdated: ((TelegramMediaFile?) -> Void)?
|
||||
@ -51,6 +60,7 @@ public final class EmojiStatusComponent: Component {
|
||||
animationCache: AnimationCache,
|
||||
animationRenderer: MultiAnimationRenderer,
|
||||
content: Content,
|
||||
isVisibleForAnimations: Bool,
|
||||
action: (() -> Void)?,
|
||||
longTapAction: (() -> Void)?,
|
||||
emojiFileUpdated: ((TelegramMediaFile?) -> Void)? = nil
|
||||
@ -59,11 +69,25 @@ public final class EmojiStatusComponent: Component {
|
||||
self.animationCache = animationCache
|
||||
self.animationRenderer = animationRenderer
|
||||
self.content = content
|
||||
self.isVisibleForAnimations = isVisibleForAnimations
|
||||
self.action = action
|
||||
self.longTapAction = longTapAction
|
||||
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 {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
@ -77,23 +101,72 @@ public final class EmojiStatusComponent: Component {
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
if lhs.isVisibleForAnimations != rhs.isVisibleForAnimations {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
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 var component: EmojiStatusComponent?
|
||||
private var iconView: UIImageView?
|
||||
private var animationLayer: InlineStickerItemLayer?
|
||||
private var lottieAnimationView: AnimationView?
|
||||
private let hierarchyTrackingLayer: HierarchyTrackingLayer
|
||||
|
||||
private var emojiFile: TelegramMediaFile?
|
||||
private var emojiFileDataProperties: AnimationFileProperties?
|
||||
private var emojiFileDisposable: Disposable?
|
||||
private var emojiFileDataPathDisposable: Disposable?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.hierarchyTrackingLayer)
|
||||
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
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) {
|
||||
@ -102,6 +175,7 @@ public final class EmojiStatusComponent: Component {
|
||||
|
||||
deinit {
|
||||
self.emojiFileDisposable?.dispose()
|
||||
self.emojiFileDataPathDisposable?.dispose()
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
@ -122,8 +196,11 @@ public final class EmojiStatusComponent: Component {
|
||||
var iconImage: UIImage?
|
||||
var emojiFileId: Int64?
|
||||
var emojiPlaceholderColor: UIColor?
|
||||
var emojiThemeColor: UIColor?
|
||||
var emojiLoopMode: LoopMode?
|
||||
var emojiSize = CGSize()
|
||||
|
||||
//let previousContent = self.component?.content
|
||||
if self.component?.content != component.content {
|
||||
switch component.content {
|
||||
case .none:
|
||||
@ -155,6 +232,7 @@ public final class EmojiStatusComponent: Component {
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
context.restoreGState()
|
||||
|
||||
context.setBlendMode(.copy)
|
||||
context.clip(to: CGRect(origin: .zero, size: size), mask: foregroundCgImage)
|
||||
context.setFillColor(foregroundColor.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
@ -167,18 +245,23 @@ public final class EmojiStatusComponent: Component {
|
||||
iconImage = nil
|
||||
case .scam:
|
||||
iconImage = nil
|
||||
case let .animation(animationContent, size, placeholderColor):
|
||||
case let .animation(animationContent, size, placeholderColor, themeColor, loopMode):
|
||||
iconImage = nil
|
||||
emojiFileId = animationContent.fileId.id
|
||||
emojiPlaceholderColor = placeholderColor
|
||||
emojiThemeColor = themeColor
|
||||
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 {
|
||||
self.emojiFileDisposable?.dispose()
|
||||
self.emojiFileDisposable = nil
|
||||
self.emojiFileDataPathDisposable?.dispose()
|
||||
self.emojiFileDataPathDisposable = nil
|
||||
|
||||
self.emojiFile = nil
|
||||
self.emojiFileDataProperties = nil
|
||||
|
||||
if let animationLayer = self.animationLayer {
|
||||
self.animationLayer = nil
|
||||
@ -192,6 +275,18 @@ public final class EmojiStatusComponent: Component {
|
||||
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 {
|
||||
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
|
||||
emojiPlaceholderColor = placeholderColor
|
||||
emojiThemeColor = themeColor
|
||||
emojiLoopMode = loopMode
|
||||
emojiSize = size
|
||||
}
|
||||
}
|
||||
@ -248,17 +345,71 @@ public final class EmojiStatusComponent: Component {
|
||||
}
|
||||
|
||||
let emojiFileUpdated = component.emojiFileUpdated
|
||||
if let emojiFileId = emojiFileId, let emojiPlaceholderColor = emojiPlaceholderColor {
|
||||
if let emojiFileId = emojiFileId, let emojiPlaceholderColor = emojiPlaceholderColor, let emojiLoopMode = emojiLoopMode {
|
||||
size = availableSize
|
||||
|
||||
let _ = emojiLoopMode
|
||||
|
||||
if let emojiFile = self.emojiFile {
|
||||
self.emojiFileDisposable?.dispose()
|
||||
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
|
||||
if let current = self.animationLayer {
|
||||
animationLayer = current
|
||||
} else {
|
||||
let loopCount: Int?
|
||||
switch emojiLoopMode {
|
||||
case .forever:
|
||||
loopCount = nil
|
||||
case let .count(value):
|
||||
loopCount = value
|
||||
}
|
||||
animationLayer = InlineStickerItemLayer(
|
||||
context: component.context,
|
||||
attemptSynchronousLoad: false,
|
||||
@ -266,8 +417,10 @@ public final class EmojiStatusComponent: Component {
|
||||
file: emojiFile,
|
||||
cache: component.animationCache,
|
||||
renderer: component.animationRenderer,
|
||||
unique: true,
|
||||
placeholderColor: emojiPlaceholderColor,
|
||||
pointSize: emojiSize
|
||||
pointSize: emojiSize,
|
||||
loopCount: loopCount
|
||||
)
|
||||
self.animationLayer = 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)
|
||||
}
|
||||
}
|
||||
|
||||
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.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 {
|
||||
if self.emojiFileDisposable == nil {
|
||||
self.emojiFileDisposable = (component.context.engine.stickers.resolveInlineStickers(fileIds: [emojiFileId])
|
||||
@ -287,6 +500,7 @@ public final class EmojiStatusComponent: Component {
|
||||
return
|
||||
}
|
||||
strongSelf.emojiFile = result[emojiFileId]
|
||||
strongSelf.emojiFileDataProperties = nil
|
||||
strongSelf.state?.updated(transition: transition)
|
||||
|
||||
emojiFileUpdated?(result[emojiFileId])
|
||||
@ -296,11 +510,14 @@ public final class EmojiStatusComponent: Component {
|
||||
} else {
|
||||
if let _ = self.emojiFile {
|
||||
self.emojiFile = nil
|
||||
self.emojiFileDataProperties = nil
|
||||
emojiFileUpdated?(nil)
|
||||
}
|
||||
|
||||
self.emojiFileDisposable?.dispose()
|
||||
self.emojiFileDisposable = nil
|
||||
self.emojiFileDataPathDisposable?.dispose()
|
||||
self.emojiFileDataPathDisposable = nil
|
||||
|
||||
if let animationLayer = self.animationLayer {
|
||||
self.animationLayer = nil
|
||||
@ -314,6 +531,18 @@ public final class EmojiStatusComponent: Component {
|
||||
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
|
||||
|
@ -19,6 +19,42 @@ import TextFormat
|
||||
import AppBundle
|
||||
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 typealias EnvironmentType = Empty
|
||||
|
||||
@ -192,6 +228,9 @@ public final class EmojiStatusSelectionController: ViewController {
|
||||
private var availableReactions: AvailableReactions?
|
||||
private var availableReactionsDisposable: Disposable?
|
||||
|
||||
private var genericReactionEffectDisposable: Disposable?
|
||||
private var genericReactionEffect: String?
|
||||
|
||||
private var hapticFeedback: HapticFeedback?
|
||||
|
||||
private var isDismissed: Bool = false
|
||||
@ -310,11 +349,17 @@ public final class EmojiStatusSelectionController: ViewController {
|
||||
}
|
||||
strongSelf.availableReactions = availableReactions
|
||||
})
|
||||
|
||||
self.genericReactionEffectDisposable = (randomGenericReactionEffect(context: context)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] path in
|
||||
self?.genericReactionEffect = path
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.emojiContentDisposable?.dispose()
|
||||
self.availableReactionsDisposable?.dispose()
|
||||
self.genericReactionEffectDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func refreshLayout(transition: Transition) {
|
||||
@ -373,42 +418,56 @@ public final class EmojiStatusSelectionController: ViewController {
|
||||
view.isOpaque = false
|
||||
|
||||
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) {
|
||||
let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
|
||||
view.animationSpeed = 1.0
|
||||
view.backgroundColor = nil
|
||||
view.isOpaque = false
|
||||
|
||||
let animationCache = self.context.animationCache
|
||||
let animationRenderer = self.context.animationRenderer
|
||||
|
||||
for i in 1 ... 7 {
|
||||
let allLayers = view.allLayers(forKeypath: AnimationKeypath(keypath: "placeholder_\(i)"))
|
||||
for animationLayer in allLayers {
|
||||
let baseItemLayer = InlineStickerItemLayer(
|
||||
context: self.context,
|
||||
attemptSynchronousLoad: false,
|
||||
emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: nil, fileId: itemFile.fileId.id, file: itemFile),
|
||||
file: item.itemFile,
|
||||
cache: animationCache,
|
||||
renderer: animationRenderer,
|
||||
placeholderColor: UIColor(white: 0.0, alpha: 0.0),
|
||||
pointSize: CGSize(width: 32.0, height: 32.0)
|
||||
)
|
||||
|
||||
if let sublayers = animationLayer.sublayers {
|
||||
for sublayer in sublayers {
|
||||
sublayer.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
baseItemLayer.isVisibleForAnimations = true
|
||||
baseItemLayer.frame = CGRect(origin: CGPoint(x: -0.0, y: -0.0), size: CGSize(width: 500.0, height: 500.0))
|
||||
animationLayer.addSublayer(baseItemLayer)
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
effectView = view
|
||||
if let effectData = effectData, let composition = try? Animation.from(data: effectData) {
|
||||
let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
|
||||
view.animationSpeed = 1.0
|
||||
view.backgroundColor = nil
|
||||
view.isOpaque = false
|
||||
|
||||
let animationCache = self.context.animationCache
|
||||
let animationRenderer = self.context.animationRenderer
|
||||
|
||||
for i in 1 ... 7 {
|
||||
let allLayers = view.allLayers(forKeypath: AnimationKeypath(keypath: "placeholder_\(i)"))
|
||||
for animationLayer in allLayers {
|
||||
let baseItemLayer = InlineStickerItemLayer(
|
||||
context: self.context,
|
||||
attemptSynchronousLoad: false,
|
||||
emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: nil, fileId: itemFile.fileId.id, file: itemFile),
|
||||
file: item.itemFile,
|
||||
cache: animationCache,
|
||||
renderer: animationRenderer,
|
||||
placeholderColor: UIColor(white: 0.0, alpha: 0.0),
|
||||
pointSize: CGSize(width: 32.0, height: 32.0)
|
||||
)
|
||||
if item.accentTint {
|
||||
baseItemLayer.contentTintColor = self.presentationData.theme.list.itemAccentColor
|
||||
}
|
||||
|
||||
if let sublayers = animationLayer.sublayers {
|
||||
for sublayer in sublayers {
|
||||
sublayer.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
baseItemLayer.isVisibleForAnimations = true
|
||||
baseItemLayer.frame = CGRect(origin: CGPoint(x: -0.0, y: -0.0), size: CGSize(width: 500.0, height: 500.0))
|
||||
animationLayer.addSublayer(baseItemLayer)
|
||||
}
|
||||
}
|
||||
|
||||
effectView = view
|
||||
}
|
||||
}
|
||||
|
||||
if let sourceCopyLayer = sourceLayer.snapshotContentTree() {
|
||||
|
@ -83,7 +83,9 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
||||
private let emoji: ChatTextInputTextCustomEmojiAttribute
|
||||
private let cache: AnimationCache
|
||||
private let renderer: MultiAnimationRenderer
|
||||
private let unique: Bool
|
||||
private let placeholderColor: UIColor
|
||||
private let loopCount: Int?
|
||||
|
||||
private let pointSize: CGSize
|
||||
private let pixelSize: CGSize
|
||||
@ -96,6 +98,16 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
||||
private var fetchDisposable: 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
|
||||
public var isVisibleForAnimations: Bool = false {
|
||||
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.emoji = emoji
|
||||
self.cache = cache
|
||||
self.renderer = renderer
|
||||
self.unique = unique
|
||||
self.placeholderColor = placeholderColor
|
||||
self.loopCount = loopCount
|
||||
|
||||
let scale = min(2.0, UIScreenScale)
|
||||
self.pointSize = pointSize
|
||||
@ -159,10 +173,28 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
||||
return nullAction
|
||||
}
|
||||
|
||||
private func updateTintColor() {
|
||||
if !self.isDisplayingPlaceholder {
|
||||
self.layerTintColor = self.contentTintColor?.cgColor
|
||||
} else {
|
||||
self.layerTintColor = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func updatePlayback() {
|
||||
let shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations
|
||||
var shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations
|
||||
|
||||
self.shouldBeAnimating = shouldBePlaying
|
||||
if shouldBePlaying, let loopCount = self.loopCount, self.currentLoopCount >= loopCount {
|
||||
shouldBePlaying = false
|
||||
}
|
||||
|
||||
if self.shouldBeAnimating != shouldBePlaying {
|
||||
self.shouldBeAnimating = shouldBePlaying
|
||||
|
||||
if !shouldBePlaying {
|
||||
self.currentLoopCount = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
self.contents = image.cgImage
|
||||
self.isDisplayingPlaceholder = true
|
||||
self.updateTintColor()
|
||||
}
|
||||
} else {
|
||||
self.updateTintColor()
|
||||
}
|
||||
|
||||
self.loadAnimation()
|
||||
@ -197,6 +232,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
||||
if let image = image {
|
||||
strongSelf.contents = image.cgImage
|
||||
strongSelf.isDisplayingPlaceholder = true
|
||||
strongSelf.updateTintColor()
|
||||
}
|
||||
|
||||
if isFinal {
|
||||
@ -224,9 +260,9 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
||||
if file.isAnimatedSticker || file.isVideoEmoji {
|
||||
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 {
|
||||
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
|
||||
guard result.complete else {
|
||||
return
|
||||
@ -250,11 +286,13 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
||||
return
|
||||
}
|
||||
self.isDisplayingPlaceholder = displayPlaceholder
|
||||
self.updateTintColor()
|
||||
}
|
||||
|
||||
override public func transitionToContents(_ contents: AnyObject) {
|
||||
override public func transitionToContents(_ contents: AnyObject, didLoop: Bool) {
|
||||
if self.isDisplayingPlaceholder {
|
||||
self.isDisplayingPlaceholder = false
|
||||
self.updateTintColor()
|
||||
|
||||
if let current = self.contents {
|
||||
let previousLayer = SimpleLayer()
|
||||
@ -275,6 +313,13 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
||||
} else {
|
||||
self.contents = contents
|
||||
}
|
||||
|
||||
if didLoop {
|
||||
self.currentLoopCount += 1
|
||||
if let loopCount = self.loopCount, self.currentLoopCount >= loopCount {
|
||||
self.updatePlayback()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1455,7 +1455,7 @@ private final class GroupExpandActionButton: UIButton {
|
||||
let textConstrainedWidth: CGFloat = 100.0
|
||||
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
|
||||
|
||||
let textSize: CGSize
|
||||
@ -1662,19 +1662,22 @@ public final class EmojiPagerContentComponent: Component {
|
||||
public let itemFile: TelegramMediaFile?
|
||||
public let subgroupId: Int32?
|
||||
public let icon: Icon
|
||||
public let accentTint: Bool
|
||||
|
||||
public init(
|
||||
animationData: EntityKeyboardAnimationData?,
|
||||
content: ItemContent,
|
||||
itemFile: TelegramMediaFile?,
|
||||
subgroupId: Int32?,
|
||||
icon: Icon
|
||||
icon: Icon,
|
||||
accentTint: Bool
|
||||
) {
|
||||
self.animationData = animationData
|
||||
self.content = content
|
||||
self.itemFile = itemFile
|
||||
self.subgroupId = subgroupId
|
||||
self.icon = icon
|
||||
self.accentTint = accentTint
|
||||
}
|
||||
|
||||
public static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
@ -1696,6 +1699,9 @@ public final class EmojiPagerContentComponent: Component {
|
||||
if lhs.icon != rhs.icon {
|
||||
return false
|
||||
}
|
||||
if lhs.accentTint != rhs.accentTint {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@ -2252,7 +2258,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
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 {
|
||||
@ -2440,7 +2446,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
self.onUpdateDisplayPlaceholder(displayPlaceholder, 0.0)
|
||||
}
|
||||
|
||||
public override func transitionToContents(_ contents: AnyObject) {
|
||||
public override func transitionToContents(_ contents: AnyObject, didLoop: Bool) {
|
||||
self.contents = contents
|
||||
|
||||
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)
|
||||
|
||||
if item.accentTint {
|
||||
itemLayer.layerTintColor = keyboardChildEnvironment.theme.list.itemAccentColor.cgColor
|
||||
} else {
|
||||
itemLayer.layerTintColor = nil
|
||||
}
|
||||
|
||||
if let placeholderView = self.visibleItemPlaceholderViews[itemId] {
|
||||
if placeholderView.layer.position != itemPosition || placeholderView.layer.bounds != itemBounds {
|
||||
itemTransition.setFrame(view: placeholderView, frame: itemFrame)
|
||||
@ -4019,7 +4031,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
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.frame = baseItemFrame
|
||||
}
|
||||
@ -4798,7 +4810,8 @@ public final class EmojiPagerContentComponent: Component {
|
||||
content: .icon(.premiumStar),
|
||||
itemFile: nil,
|
||||
subgroupId: nil,
|
||||
icon: .none
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
)
|
||||
|
||||
let groupId = "recent"
|
||||
@ -4822,6 +4835,20 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
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 animationData = EntityKeyboardAnimationData(file: file)
|
||||
@ -4830,7 +4857,8 @@ public final class EmojiPagerContentComponent: Component {
|
||||
content: .animation(animationData),
|
||||
itemFile: file,
|
||||
subgroupId: nil,
|
||||
icon: .none
|
||||
icon: .none,
|
||||
accentTint: accentTint
|
||||
)
|
||||
|
||||
if let groupIndex = itemGroupIndexById[groupId] {
|
||||
@ -4852,13 +4880,28 @@ public final class EmojiPagerContentComponent: Component {
|
||||
|
||||
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)
|
||||
resultItem = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: file,
|
||||
subgroupId: nil,
|
||||
icon: .none
|
||||
icon: .none,
|
||||
accentTint: accentTint
|
||||
)
|
||||
|
||||
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 {
|
||||
if existingIds.contains(reactionItem.reaction) {
|
||||
continue
|
||||
@ -4911,14 +4961,15 @@ public final class EmojiPagerContentComponent: Component {
|
||||
content: .animation(animationData),
|
||||
itemFile: animationFile,
|
||||
subgroupId: nil,
|
||||
icon: icon
|
||||
icon: icon,
|
||||
accentTint: false
|
||||
)
|
||||
|
||||
let groupId = "recent"
|
||||
if let groupIndex = itemGroupIndexById[groupId] {
|
||||
itemGroups[groupIndex].items.append(resultItem)
|
||||
|
||||
if itemGroups[groupIndex].items.count >= 8 * 2 {
|
||||
if itemGroups[groupIndex].items.count >= 8 * maxTopLineCount {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
@ -4966,7 +5017,8 @@ public final class EmojiPagerContentComponent: Component {
|
||||
content: .animation(animationData),
|
||||
itemFile: animationFile,
|
||||
subgroupId: nil,
|
||||
icon: icon
|
||||
icon: icon,
|
||||
accentTint: false
|
||||
)
|
||||
|
||||
if hasPremium {
|
||||
@ -5040,7 +5092,8 @@ public final class EmojiPagerContentComponent: Component {
|
||||
content: .animation(animationData),
|
||||
itemFile: animationFile,
|
||||
subgroupId: nil,
|
||||
icon: icon
|
||||
icon: icon,
|
||||
accentTint: false
|
||||
)
|
||||
|
||||
let groupId = "popular"
|
||||
@ -5082,7 +5135,8 @@ public final class EmojiPagerContentComponent: Component {
|
||||
content: .animation(animationData),
|
||||
itemFile: file,
|
||||
subgroupId: nil,
|
||||
icon: .none
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
)
|
||||
case let .text(text):
|
||||
resultItem = EmojiPagerContentComponent.Item(
|
||||
@ -5090,7 +5144,8 @@ public final class EmojiPagerContentComponent: Component {
|
||||
content: .staticEmoji(text),
|
||||
itemFile: nil,
|
||||
subgroupId: nil,
|
||||
icon: .none
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
)
|
||||
}
|
||||
|
||||
@ -5113,7 +5168,8 @@ public final class EmojiPagerContentComponent: Component {
|
||||
content: .staticEmoji(emojiString),
|
||||
itemFile: nil,
|
||||
subgroupId: subgroupId.rawValue,
|
||||
icon: .none
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
)
|
||||
|
||||
if let groupIndex = itemGroupIndexById[groupId] {
|
||||
@ -5148,7 +5204,8 @@ public final class EmojiPagerContentComponent: Component {
|
||||
content: .animation(animationData),
|
||||
itemFile: item.file,
|
||||
subgroupId: nil,
|
||||
icon: icon
|
||||
icon: icon,
|
||||
accentTint: false
|
||||
)
|
||||
|
||||
let supergroupId = entry.index.collectionId
|
||||
@ -5208,7 +5265,8 @@ public final class EmojiPagerContentComponent: Component {
|
||||
content: .animation(animationData),
|
||||
itemFile: item.file,
|
||||
subgroupId: nil,
|
||||
icon: .none
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
)
|
||||
|
||||
let supergroupId = featuredEmojiPack.info.id
|
||||
|
@ -115,7 +115,8 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
|
||||
content: .animation(component.item),
|
||||
itemFile: nil,
|
||||
subgroupId: nil,
|
||||
icon: .none
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
),
|
||||
context: component.context,
|
||||
attemptSynchronousLoad: false,
|
||||
|
@ -398,7 +398,7 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
|
||||
let preferredRowAlignment = self.preferredRowAlignment
|
||||
|
||||
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 textureU = readyTextureU ?? TextureStoragePool.takeNew(device: device, parameters: halfParameters, pool: texturePoolHalfPlane)
|
||||
@ -517,7 +517,7 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
@ -798,13 +798,13 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
|
||||
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)
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -818,7 +818,7 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer {
|
||||
strongSelf.updateIsPlaying()
|
||||
})
|
||||
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
|
||||
} else {
|
||||
return EmptyDisposable
|
||||
|
@ -6,7 +6,7 @@ import AnimationCache
|
||||
import Accelerate
|
||||
|
||||
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 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)
|
||||
@ -73,7 +73,7 @@ open class MultiAnimationRenderTarget: SimpleLayer {
|
||||
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 {
|
||||
let result = item.advance(advance: .frames(1), requestedFormat: .rgba)
|
||||
if i == index {
|
||||
return result
|
||||
return result?.frame
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@ -294,7 +294,7 @@ private final class ItemAnimationContext {
|
||||
|
||||
for target in self.targets.copyItems() {
|
||||
if let target = target.value {
|
||||
target.transitionToContents(currentFrame.image.cgImage!)
|
||||
target.transitionToContents(currentFrame.image.cgImage!, didLoop: false)
|
||||
|
||||
if let blurredRepresentationTarget = target.blurredRepresentationTarget {
|
||||
blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage
|
||||
@ -305,7 +305,7 @@ private final class ItemAnimationContext {
|
||||
} else {
|
||||
for target in self.targets.copyItems() {
|
||||
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) {
|
||||
if let currentFrame = self.currentFrame {
|
||||
if let cgImage = currentFrame.image.cgImage {
|
||||
target.transitionToContents(cgImage)
|
||||
target.transitionToContents(cgImage, didLoop: false)
|
||||
|
||||
if let blurredRepresentationTarget = target.blurredRepresentationTarget {
|
||||
blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage
|
||||
@ -384,10 +384,16 @@ private final class ItemAnimationContext {
|
||||
self.loadingFrameTaskId = taskId
|
||||
|
||||
return LoadFrameGroupTask(task: { [weak self] in
|
||||
let currentFrame: Frame?
|
||||
let currentFrame: (frame: Frame, didLoop: Bool)?
|
||||
do {
|
||||
if let frame = try item.tryWith({ $0.advance(advance: frameAdvance, requestedFormat: .rgba) }) {
|
||||
currentFrame = Frame(frame: frame)
|
||||
if let (frame, didLoop) = try item.tryWith({ item -> (AnimationCacheItemFrame, Bool)? in
|
||||
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 {
|
||||
currentFrame = nil
|
||||
}
|
||||
@ -408,13 +414,13 @@ private final class ItemAnimationContext {
|
||||
strongSelf.loadingFrameTaskId = nil
|
||||
|
||||
if let currentFrame = currentFrame {
|
||||
strongSelf.currentFrame = currentFrame
|
||||
strongSelf.currentFrame = currentFrame.frame
|
||||
for target in strongSelf.targets.copyItems() {
|
||||
if let target = target.value {
|
||||
target.transitionToContents(currentFrame.image.cgImage!)
|
||||
target.transitionToContents(currentFrame.frame.image.cgImage!, didLoop: currentFrame.didLoop)
|
||||
|
||||
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 width: Int
|
||||
var height: Int
|
||||
var uniqueId: Int
|
||||
}
|
||||
|
||||
private var itemContexts: [ItemKey: ItemAnimationContext] = [:]
|
||||
private var nextQueueAffinity: Int = 0
|
||||
private var nextUniqueId: Int = 1
|
||||
|
||||
private(set) var isPlaying: Bool = false {
|
||||
didSet {
|
||||
@ -462,8 +470,14 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
||||
self.stateUpdated = stateUpdated
|
||||
}
|
||||
|
||||
func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable {
|
||||
let itemKey = ItemKey(id: itemId, width: Int(size.width), height: Int(size.height))
|
||||
func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable {
|
||||
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
|
||||
if let current = self.itemContexts[itemKey] {
|
||||
itemContext = current
|
||||
@ -521,7 +535,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
||||
guard let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) else {
|
||||
return false
|
||||
}
|
||||
guard let loadedFrame = ItemAnimationContext.Frame(frame: frame) else {
|
||||
guard let loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) else {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -551,7 +565,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
||||
|
||||
let loadedFrame: ItemAnimationContext.Frame?
|
||||
if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) {
|
||||
loadedFrame = ItemAnimationContext.Frame(frame: frame)
|
||||
loadedFrame = ItemAnimationContext.Frame(frame: frame.frame)
|
||||
} else {
|
||||
loadedFrame = nil
|
||||
}
|
||||
@ -564,7 +578,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
||||
if let loadedFrame = loadedFrame {
|
||||
if let cgImage = loadedFrame.image.cgImage {
|
||||
if hadIntermediateUpdate {
|
||||
target.transitionToContents(cgImage)
|
||||
target.transitionToContents(cgImage, didLoop: false)
|
||||
} else {
|
||||
target.contents = cgImage
|
||||
}
|
||||
@ -583,7 +597,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
if let current = self.groupContext {
|
||||
groupContext = current
|
||||
@ -678,7 +692,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
||||
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 {
|
||||
disposable.dispose()
|
||||
|
@ -1667,7 +1667,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@ -6733,7 +6733,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
|
@ -201,7 +201,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
content: .animation(animationData),
|
||||
itemFile: item.file,
|
||||
subgroupId: nil,
|
||||
icon: .none
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
)
|
||||
|
||||
let supergroupId = "featuredTop"
|
||||
@ -238,7 +239,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
content: .animation(animationData),
|
||||
itemFile: item.file,
|
||||
subgroupId: nil,
|
||||
icon: .none
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
)
|
||||
|
||||
let groupId = "saved"
|
||||
@ -266,7 +268,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
content: .animation(animationData),
|
||||
itemFile: item.media,
|
||||
subgroupId: nil,
|
||||
icon: .none
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
)
|
||||
|
||||
let groupId = "recent"
|
||||
@ -317,7 +320,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
content: .animation(animationData),
|
||||
itemFile: item.file,
|
||||
subgroupId: nil,
|
||||
icon: .none
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
)
|
||||
|
||||
let groupId = "premium"
|
||||
@ -350,7 +354,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
content: .animation(animationData),
|
||||
itemFile: item.file,
|
||||
subgroupId: nil,
|
||||
icon: .none
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
)
|
||||
|
||||
let groupId = "peerSpecific"
|
||||
@ -373,7 +378,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
content: .animation(animationData),
|
||||
itemFile: item.file,
|
||||
subgroupId: nil,
|
||||
icon: .none
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
)
|
||||
let groupId = entry.index.collectionId
|
||||
if let groupIndex = itemGroupIndexById[groupId] {
|
||||
@ -426,7 +432,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
content: .animation(animationData),
|
||||
itemFile: item.file,
|
||||
subgroupId: nil,
|
||||
icon: .none
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
)
|
||||
|
||||
let supergroupId = featuredStickerPack.info.id
|
||||
|
@ -585,6 +585,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
|
||||
private var refreshDisplayedItemRangeTimer: SwiftSignalKit.Timer?
|
||||
|
||||
private var genericReactionEffect: String?
|
||||
private var genericReactionEffectDisposable: Disposable?
|
||||
|
||||
private var visibleMessageRange = Atomic<VisibleMessageRange?>(value: nil)
|
||||
|
||||
private let clientId: Atomic<Int32>
|
||||
@ -1568,6 +1571,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
return strongSelf.isSelectionGestureEnabled
|
||||
}
|
||||
self.view.addGestureRecognizer(selectionRecognizer)
|
||||
|
||||
self.loadNextGenericReactionEffect(context: context)
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -1579,6 +1584,24 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
self.loadedMessagesFromCachedDataDisposable?.dispose()
|
||||
self.preloadAdPeerDisposable.dispose()
|
||||
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) {
|
||||
@ -2724,7 +2747,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
}
|
||||
|
||||
if reaction.value == updatedReaction {
|
||||
let standaloneReactionAnimation = StandaloneReactionAnimation()
|
||||
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: self.genericReactionEffect)
|
||||
|
||||
chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
|
||||
|
||||
|
@ -26,6 +26,8 @@ import ChatPresentationInterfaceState
|
||||
import ChatMessageBackground
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import ComponentFlow
|
||||
import EmojiStatusComponent
|
||||
|
||||
enum InternalBubbleTapAction {
|
||||
case action(() -> Void)
|
||||
@ -488,7 +490,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
|
||||
private var nameNode: TextNode?
|
||||
private var adminBadgeNode: TextNode?
|
||||
private var credibilityIconNode: ASImageNode?
|
||||
private var credibilityIconView: ComponentHostView<Empty>?
|
||||
private var credibilityIconComponent: EmojiStatusComponent?
|
||||
private var forwardInfoNode: ChatMessageForwardInfoNode?
|
||||
var forwardInfoReferenceNode: ASDisplayNode? {
|
||||
return self.forwardInfoNode
|
||||
@ -532,6 +535,23 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
if let replyInfoNode = self.replyInfoNode {
|
||||
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
|
||||
}
|
||||
|
||||
var currentCredibilityIconImage: UIImage?
|
||||
var currentCredibilityIcon: EmojiStatusComponent.Content?
|
||||
|
||||
var initialDisplayHeader = true
|
||||
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 {
|
||||
|
||||
} 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 {
|
||||
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
|
||||
if let credibilityIconImage = currentCredibilityIconImage {
|
||||
credibilityIconWidth += credibilityIconImage.size.width + 4.0
|
||||
if let currentCredibilityIcon = currentCredibilityIcon {
|
||||
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()))
|
||||
adminNodeSizeApply = (adminBadgeSizeAndApply.0.size, {
|
||||
@ -2249,7 +2276,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
contentOrigin: contentOrigin,
|
||||
nameNodeOriginY: nameNodeOriginY,
|
||||
layoutConstants: layoutConstants,
|
||||
currentCredibilityIconImage: currentCredibilityIconImage,
|
||||
currentCredibilityIcon: currentCredibilityIcon,
|
||||
adminNodeSizeApply: adminNodeSizeApply,
|
||||
contentUpperRightCorner: contentUpperRightCorner,
|
||||
forwardInfoSizeApply: forwardInfoSizeApply,
|
||||
@ -2293,7 +2320,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
contentOrigin: CGPoint,
|
||||
nameNodeOriginY: CGFloat,
|
||||
layoutConstants: ChatMessageItemLayoutConstants,
|
||||
currentCredibilityIconImage: UIImage?,
|
||||
currentCredibilityIcon: EmojiStatusComponent.Content?,
|
||||
adminNodeSizeApply: (CGSize, () -> TextNode?),
|
||||
contentUpperRightCorner: CGPoint,
|
||||
forwardInfoSizeApply: (CGSize, (CGFloat) -> ChatMessageForwardInfoNode?),
|
||||
@ -2411,20 +2438,38 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
animation.animator.updateFrame(layer: nameNode.layer, frame: nameNodeFrame, completion: nil)
|
||||
}
|
||||
|
||||
if let credibilityIconImage = currentCredibilityIconImage {
|
||||
let credibilityIconNode: ASImageNode
|
||||
if let node = strongSelf.credibilityIconNode {
|
||||
credibilityIconNode = node
|
||||
if let currentCredibilityIcon = currentCredibilityIcon {
|
||||
let credibilityIconView: ComponentHostView<Empty>
|
||||
if let current = strongSelf.credibilityIconView {
|
||||
credibilityIconView = current
|
||||
} else {
|
||||
credibilityIconNode = ASImageNode()
|
||||
strongSelf.credibilityIconNode = credibilityIconNode
|
||||
strongSelf.clippingNode.addSubnode(credibilityIconNode)
|
||||
credibilityIconView = ComponentHostView<Empty>()
|
||||
credibilityIconView.isUserInteractionEnabled = false
|
||||
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 {
|
||||
strongSelf.credibilityIconNode?.removeFromSupernode()
|
||||
strongSelf.credibilityIconNode = nil
|
||||
strongSelf.credibilityIconView?.removeFromSuperview()
|
||||
strongSelf.credibilityIconView = nil
|
||||
}
|
||||
|
||||
if let adminBadgeNode = adminNodeSizeApply.1() {
|
||||
|
@ -679,7 +679,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
case .scam:
|
||||
titleCredibilityContent = .scam(color: self.theme.chat.message.incoming.scamColor)
|
||||
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(
|
||||
@ -689,6 +689,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
animationCache: self.animationCache,
|
||||
animationRenderer: self.animationRenderer,
|
||||
content: titleCredibilityContent,
|
||||
isVisibleForAnimations: true,
|
||||
action: nil,
|
||||
longTapAction: nil
|
||||
)),
|
||||
|
@ -221,6 +221,8 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode {
|
||||
self.addSubnode(itemNode)
|
||||
}
|
||||
|
||||
itemNode.visibility = .visible(1.0, .infinite)
|
||||
|
||||
let height = itemNode.contentSize.height
|
||||
|
||||
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: itemNode.bounds.size))
|
||||
|
@ -2354,8 +2354,8 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
emojiExpandedStatusContent = .scam(color: presentationData.theme.chat.message.incoming.scamColor)
|
||||
case let .emojiStatus(emojiStatus):
|
||||
currentEmojiStatus = emojiStatus
|
||||
emojiRegularStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: presentationData.theme.list.mediaPlaceholderColor)
|
||||
emojiExpandedStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: UIColor(rgb: 0xffffff, alpha: 0.15))
|
||||
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), themeColor: presentationData.theme.list.itemAccentColor, loopMode: .forever)
|
||||
}
|
||||
|
||||
let animateStatusIcon = !self.titleCredibilityIconView.bounds.isEmpty
|
||||
@ -2367,6 +2367,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
animationCache: self.animationCache,
|
||||
animationRenderer: self.animationRenderer,
|
||||
content: emojiRegularStatusContent,
|
||||
isVisibleForAnimations: true,
|
||||
action: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -2427,6 +2428,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
animationCache: self.animationCache,
|
||||
animationRenderer: self.animationRenderer,
|
||||
content: emojiExpandedStatusContent,
|
||||
isVisibleForAnimations: true,
|
||||
action: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
Loading…
x
Reference in New Issue
Block a user