diff --git a/submodules/ChatListUI/Sources/ChatListTitleView.swift b/submodules/ChatListUI/Sources/ChatListTitleView.swift index 256e127b14..add4869b1c 100644 --- a/submodules/ChatListUI/Sources/ChatListTitleView.swift +++ b/submodules/ChatListUI/Sources/ChatListTitleView.swift @@ -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 diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index f46135702b..9adf7d4795 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -460,6 +460,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let pinnedIconNode: ASImageNode var secretIconNode: ASImageNode? var credibilityIconView: ComponentHostView? + 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) ) diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index e71b760fa3..ada8d1090c 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -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) } diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index e22f096091..453ab9577a 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -370,6 +370,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private let avatarNode: AvatarNode private let titleNode: TextNode private var credibilityIconView: ComponentHostView? + 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) ) diff --git a/submodules/Display/Source/UIKitUtils.swift b/submodules/Display/Source/UIKitUtils.swift index 2d868f8b1a..13b36c6daa 100644 --- a/submodules/Display/Source/UIKitUtils.swift +++ b/submodules/Display/Source/UIKitUtils.swift @@ -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 diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index 4d8df362d9..11cb4668dd 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -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? 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) ) diff --git a/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift b/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift index a4f1d2737a..8f95ad4bf8 100644 --- a/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift +++ b/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift @@ -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 } diff --git a/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift b/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift index 238fae79a6..1eb5619f24 100644 --- a/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift +++ b/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift @@ -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 )), diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 732f2a3428..405d534f73 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -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 diff --git a/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift b/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift index 0bb873af3e..876e0c458c 100644 --- a/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift +++ b/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift @@ -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) diff --git a/submodules/ReactionSelectionNode/BUILD b/submodules/ReactionSelectionNode/BUILD index 64d2108cf9..00e8cc7db3 100644 --- a/submodules/ReactionSelectionNode/BUILD +++ b/submodules/ReactionSelectionNode/BUILD @@ -32,6 +32,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", "//submodules/TextFormat:TextFormat", + "//submodules/GZip:GZip", ], visibility = [ "//visibility:public", diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index fe910f207b..7b30be4e18 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -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 { + 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 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)?, 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) diff --git a/submodules/SettingsUI/Sources/Reactions/ItemListReactionItem.swift b/submodules/SettingsUI/Sources/Reactions/ItemListReactionItem.swift index fc07438fcd..d878e5ab03 100644 --- a/submodules/SettingsUI/Sources/Reactions/ItemListReactionItem.swift +++ b/submodules/SettingsUI/Sources/Reactions/ItemListReactionItem.swift @@ -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 diff --git a/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift b/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift index c23d823a40..4a918adcc9 100644 --- a/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift @@ -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 diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift index b468ceff9a..10342c97cc 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift @@ -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, diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 01bfcc7fa4..8011dc7b67 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -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) } diff --git a/submodules/TelegramApi/Sources/Api9.swift b/submodules/TelegramApi/Sources/Api9.swift index 3abcb9bab0..6d783fafc2 100644 --- a/submodules/TelegramApi/Sources/Api9.swift +++ b/submodules/TelegramApi/Sources/Api9.swift @@ -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 } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift index bca04443eb..b4385885ad 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift @@ -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 diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index f8e274a16b..c806cb92a9 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -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 diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift index 4cea03247a..2b46980647 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift @@ -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 } } } diff --git a/submodules/TelegramCore/Sources/State/ManagedAnimatedEmojiUpdates.swift b/submodules/TelegramCore/Sources/State/ManagedAnimatedEmojiUpdates.swift index 10ad50d969..e2950d57b5 100644 --- a/submodules/TelegramCore/Sources/State/ManagedAnimatedEmojiUpdates.swift +++ b/submodules/TelegramCore/Sources/State/ManagedAnimatedEmojiUpdates.swift @@ -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 { + let poll = _internal_loadedStickerPack(postbox: postbox, network: network, reference: .emojiGenericAnimations, forceActualized: true) + |> mapToSignal { _ -> Signal in + return .complete() + } + return (poll |> then(.complete() |> suspendAwareDelay(2.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart +} diff --git a/submodules/TelegramCore/Sources/State/SynchronizeSavedStickersOperation.swift b/submodules/TelegramCore/Sources/State/SynchronizeSavedStickersOperation.swift index 7f633325d0..df43c43664 100644 --- a/submodules/TelegramCore/Sources/State/SynchronizeSavedStickersOperation.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizeSavedStickersOperation.swift @@ -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)) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 5279db8f1f..60fa94d66c 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -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 { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift index 587ffed885..df92af2fa0 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift @@ -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 + } } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift index 6a0b558e0e..84153830eb 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift @@ -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 } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift index 39f7ef1f0d..269643129e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift @@ -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 } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift index ef411ee005..7246c96425 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift @@ -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 diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift index da15a9b3b0..f399b17df4 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift @@ -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), diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift index 0e24dbf89a..363398ba67 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift @@ -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), diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index 6d9dd8741b..9a37db495f 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -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), diff --git a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift index d04a40c8e7..c8bf5f50ae 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift @@ -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 + ) } } diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift index 3263541e8c..352f8db940 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift @@ -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) diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift index d0b09d7f3f..1469dbc2a6 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift @@ -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) } } diff --git a/submodules/TelegramUI/Components/EmojiStatusComponent/BUILD b/submodules/TelegramUI/Components/EmojiStatusComponent/BUILD index 824a18d505..946cb68b8c 100644 --- a/submodules/TelegramUI/Components/EmojiStatusComponent/BUILD +++ b/submodules/TelegramUI/Components/EmojiStatusComponent/BUILD @@ -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", diff --git a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift index dcacc229f8..3429bb7766 100644 --- a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift @@ -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 { 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 diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index 54ac66dfb1..160851c854 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -19,6 +19,42 @@ import TextFormat import AppBundle import GZip +private func randomGenericReactionEffect(context: AccountContext) -> Signal { + 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 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() { diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index ad64f10247..5a3b255956 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -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() + } + } } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 040ce2b079..e0cea82cfc 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -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 diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index 825daacf0d..4d3e5152f4 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -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, diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift index 29f2465503..76feb978c6 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift @@ -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 diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift index 0927adc1b7..a9d3598093 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -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() diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 6d0eff7c3c..f34f12fe0d 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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) diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index 33cbf1da58..5e89d65692 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 08421b9bde..c8a74ff0b6 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -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(value: nil) private let clientId: Atomic @@ -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) diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 4afdfe4501..1ac87ed380 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -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? + 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 + if let current = strongSelf.credibilityIconView { + credibilityIconView = current } else { - credibilityIconNode = ASImageNode() - strongSelf.credibilityIconNode = credibilityIconNode - strongSelf.clippingNode.addSubnode(credibilityIconNode) + credibilityIconView = ComponentHostView() + 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() { diff --git a/submodules/TelegramUI/Sources/ChatTitleView.swift b/submodules/TelegramUI/Sources/ChatTitleView.swift index d8c04cd91d..76e39a89df 100644 --- a/submodules/TelegramUI/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Sources/ChatTitleView.swift @@ -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 )), diff --git a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift index 457e781c73..8330e13cb7 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift @@ -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)) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 2ae6f556c7..94af790fcf 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -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