Emoji status and reaction improvements

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

View File

@ -118,7 +118,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
case .premium:
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

View File

@ -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)
)

View File

@ -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)
}

View File

@ -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)
)

View File

@ -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

View File

@ -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)
)

View File

@ -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
}

View File

@ -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
)),

View File

@ -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

View File

@ -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)

View File

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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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) }

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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
}
}
}

View File

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

View File

@ -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))

View File

@ -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 {

View File

@ -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
}
}
}
}

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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),

View File

@ -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),

View File

@ -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),

View File

@ -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
)
}
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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",

View File

@ -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

View File

@ -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() {

View File

@ -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()
}
}
}
}

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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() {

View File

@ -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
)),

View File

@ -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))

View File

@ -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