From 04ea87c1e35b5688d85c629080c135fe29206f6f Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 5 Aug 2022 23:16:30 +0400 Subject: [PATCH] [WIP] Emoji statuses --- submodules/ChatListUI/BUILD | 1 + .../Sources/Node/ChatListItem.swift | 52 +++ .../Source/Base/Transition.swift | 53 ++- submodules/Display/Source/SimpleLayer.swift | 26 ++ .../SyncCore/SyncCore_CachedUserData.swift | 60 ++- .../TelegramEngineAccountData.swift | 18 + submodules/TelegramUI/BUILD | 2 + .../Components/EmojiStatusComponent/BUILD | 28 ++ .../Sources/EmojiStatusComponent.swift | 341 ++++++++++++++ .../EmojiStatusSelectionComponent/BUILD | 28 ++ .../EmojiStatusSelectionComponent.swift | 437 ++++++++++++++++++ .../Sources/EmojiPagerContentComponent.swift | 65 +++ .../Sources/EntityKeyboard.swift | 28 +- .../EntityKeyboardTopPanelComponent.swift | 17 +- .../TelegramUI/Sources/ChatController.swift | 2 +- .../Sources/ChatEntityKeyboardInputNode.swift | 42 +- .../TelegramUI/Sources/ChatTitleView.swift | 98 ++-- .../Sources/PeerInfo/PeerInfoHeaderNode.swift | 164 +++++-- .../Sources/PeerInfo/PeerInfoScreen.swift | 27 ++ .../Source/UIKitRuntimeUtils/UIKitUtils.m | 4 +- 20 files changed, 1377 insertions(+), 116 deletions(-) create mode 100644 submodules/TelegramUI/Components/EmojiStatusComponent/BUILD create mode 100644 submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift create mode 100644 submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD create mode 100644 submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index a88150a88e..fb2e3f1932 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -74,6 +74,7 @@ swift_library( "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer", "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TelegramUI/Components/EmojiStatusComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 5f27925810..42888a4728 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -25,6 +25,8 @@ import UniversalMediaPlayer import GalleryUI import HierarchyTrackingLayer import TextNodeWithEntities +import ComponentFlow +import EmojiStatusComponent public enum ChatListItemContent { public final class DraftState: Equatable { @@ -458,6 +460,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let pinnedIconNode: ASImageNode var secretIconNode: ASImageNode? var credibilityIconNode: ASImageNode? + var credibilityIconView: ComponentHostView? let mutedIconNode: ASImageNode private var hierarchyTrackingLayer: HierarchyTrackingLayer? @@ -1069,6 +1072,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var currentPinnedIconImage: UIImage? var currentMutedIconImage: UIImage? var currentCredibilityIconImage: UIImage? + var currentCredibilityIconContent: EmojiStatusComponent.Content? var currentSecretIconImage: UIImage? var selectableControlSizeAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)? @@ -1515,12 +1519,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if let peer = messages.last?.author { if peer.isScam { currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) + currentCredibilityIconContent = .scam(color: item.presentationData.theme.chat.message.incoming.scamColor) } else if peer.isFake { currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) + currentCredibilityIconContent = .fake(color: item.presentationData.theme.chat.message.incoming.scamColor) } else if peer.isVerified { currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme) + currentCredibilityIconContent = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled { currentCredibilityIconImage = PresentationResourcesChatList.premiumIcon(item.presentationData.theme) + + if "".isEmpty { + currentCredibilityIconContent = .emojiStatus(status: PeerEmojiStatus(fileId: 5431449001532594346), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor) + } else { + currentCredibilityIconContent = .premium(color: item.presentationData.theme.list.itemAccentColor) + } } } default: @@ -1529,12 +1542,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer { if peer.isScam { currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) + currentCredibilityIconContent = .scam(color: item.presentationData.theme.chat.message.incoming.scamColor) } else if peer.isFake { currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) + currentCredibilityIconContent = .fake(color: item.presentationData.theme.chat.message.incoming.scamColor) } else if peer.isVerified { currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme) + currentCredibilityIconContent = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled { currentCredibilityIconImage = PresentationResourcesChatList.premiumIcon(item.presentationData.theme) + + if "".isEmpty { + currentCredibilityIconContent = .emojiStatus(status: PeerEmojiStatus(fileId: 5431449001532594346), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor) + } else { + currentCredibilityIconContent = .premium(color: item.presentationData.theme.list.itemAccentColor) + } } } } @@ -2030,6 +2052,35 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } var nextTitleIconOrigin: CGFloat = contentRect.origin.x + titleLayout.size.width + 3.0 + titleOffset + + if let currentCredibilityIconContent = currentCredibilityIconContent { + let credibilityIconView: ComponentHostView + if let current = strongSelf.credibilityIconView { + credibilityIconView = current + } else { + credibilityIconView = ComponentHostView() + strongSelf.credibilityIconView = credibilityIconView + strongSelf.contextContainer.view.addSubview(credibilityIconView) + } + 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 + )), + environment: {}, + containerSize: CGSize(width: 20.0, height: 20.0) + ) + transition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0) - UIScreenPixel), size: iconSize)) + } else if let credibilityIconView = strongSelf.credibilityIconView { + strongSelf.credibilityIconView = nil + credibilityIconView.removeFromSuperview() + } + if let currentCredibilityIconImage = currentCredibilityIconImage { let iconNode: ASImageNode if let current = strongSelf.credibilityIconNode { @@ -2041,6 +2092,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { iconNode.displayWithoutProcessing = true strongSelf.contextContainer.addSubnode(iconNode) strongSelf.credibilityIconNode = iconNode + iconNode.isHidden = true } iconNode.image = currentCredibilityIconImage transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: floorToScreenPixels(titleFrame.midY - currentCredibilityIconImage.size.height / 2.0) - UIScreenPixel), size: currentCredibilityIconImage.size)) diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index c2ca5bc861..c6a8334e42 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -369,19 +369,23 @@ public struct Transition { } public func setAlpha(view: UIView, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) { - if view.alpha == alpha { + self.setAlpha(layer: view.layer, alpha: alpha, completion: completion) + } + + public func setAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) { + if layer.opacity == Float(alpha) { completion?(true) return } switch self.animation { case .none: - view.alpha = alpha - view.layer.removeAnimation(forKey: "opacity") + layer.opacity = Float(alpha) + layer.removeAnimation(forKey: "opacity") completion?(true) case .curve: - let previousAlpha = (view.layer.presentation()?.opacity).flatMap(CGFloat.init) ?? view.alpha - view.alpha = alpha - self.animateAlpha(view: view, from: previousAlpha, to: alpha, completion: completion) + let previousAlpha = layer.presentation()?.opacity ?? layer.opacity + layer.opacity = Float(alpha) + self.animateAlpha(layer: layer, from: CGFloat(previousAlpha), to: alpha, completion: completion) } } @@ -413,6 +417,37 @@ public struct Transition { } } + public func setTransform(view: UIView, transform: CATransform3D, completion: ((Bool) -> Void)? = nil) { + self.setTransform(layer: view.layer, transform: transform, completion: completion) + } + + public func setTransform(layer: CALayer, transform: CATransform3D, completion: ((Bool) -> Void)? = nil) { + switch self.animation { + case .none: + layer.transform = transform + completion?(true) + case let .curve(duration, curve): + let previousValue: CATransform3D + if let presentation = layer.presentation() { + previousValue = presentation.transform + } else { + previousValue = layer.transform + } + layer.transform = transform + layer.animate( + from: NSValue(caTransform3D: previousValue), + to: NSValue(caTransform3D: transform), + keyPath: "transform", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: false, + completion: completion + ) + } + } + public func setSublayerTransform(view: UIView, transform: CATransform3D, completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: @@ -479,11 +514,15 @@ public struct Transition { } public func animateAlpha(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + self.animateAlpha(layer: view.layer, from: fromValue, to: toValue, additive: additive, completion: completion) + } + + public func animateAlpha(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: completion?(true) case let .curve(duration, curve): - view.layer.animate( + layer.animate( from: fromValue as NSNumber, to: toValue as NSNumber, keyPath: "opacity", diff --git a/submodules/Display/Source/SimpleLayer.swift b/submodules/Display/Source/SimpleLayer.swift index 740d3965de..8bc691303b 100644 --- a/submodules/Display/Source/SimpleLayer.swift +++ b/submodules/Display/Source/SimpleLayer.swift @@ -61,3 +61,29 @@ open class SimpleShapeLayer: CAShapeLayer { fatalError("init(coder:) has not been implemented") } } + +open class SimpleGradientLayer: CAGradientLayer { + public var didEnterHierarchy: (() -> Void)? + public var didExitHierarchy: (() -> Void)? + + override open func action(forKey event: String) -> CAAction? { + if event == kCAOnOrderIn { + self.didEnterHierarchy?() + } else if event == kCAOnOrderOut { + self.didExitHierarchy?() + } + return nullAction + } + + override public init() { + super.init() + } + + override public init(layer: Any) { + super.init(layer: layer) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index fb0366c200..b29d32d40e 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -85,6 +85,14 @@ public struct CachedPremiumGiftOption: Equatable, PostboxCoding { } } +public struct PeerEmojiStatus: Equatable, Codable { + public var fileId: Int64 + + public init(fileId: Int64) { + self.fileId = fileId + } +} + public final class CachedUserData: CachedPeerData { public let about: String? public let botInfo: BotInfo? @@ -102,6 +110,7 @@ public final class CachedUserData: CachedPeerData { public let photo: TelegramMediaImage? public let premiumGiftOptions: [CachedPremiumGiftOption] public let voiceMessagesAvailable: Bool + public let emojiStatus: PeerEmojiStatus? public let peerIds: Set public let messageIds: Set @@ -124,11 +133,12 @@ public final class CachedUserData: CachedPeerData { self.photo = nil self.premiumGiftOptions = [] self.voiceMessagesAvailable = true + self.emojiStatus = nil self.peerIds = Set() self.messageIds = Set() } - public init(about: String?, botInfo: BotInfo?, peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, isBlocked: Bool, commonGroupCount: Int32, voiceCallsAvailable: Bool, videoCallsAvailable: Bool, callsPrivate: Bool, canPinMessages: Bool, hasScheduledMessages: Bool, autoremoveTimeout: CachedPeerAutoremoveTimeout, themeEmoticon: String?, photo: TelegramMediaImage?, premiumGiftOptions: [CachedPremiumGiftOption], voiceMessagesAvailable: Bool) { + public init(about: String?, botInfo: BotInfo?, peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, isBlocked: Bool, commonGroupCount: Int32, voiceCallsAvailable: Bool, videoCallsAvailable: Bool, callsPrivate: Bool, canPinMessages: Bool, hasScheduledMessages: Bool, autoremoveTimeout: CachedPeerAutoremoveTimeout, themeEmoticon: String?, photo: TelegramMediaImage?, premiumGiftOptions: [CachedPremiumGiftOption], voiceMessagesAvailable: Bool, emojiStatus: PeerEmojiStatus?) { self.about = about self.botInfo = botInfo self.peerStatusSettings = peerStatusSettings @@ -145,6 +155,7 @@ public final class CachedUserData: CachedPeerData { self.photo = photo self.premiumGiftOptions = premiumGiftOptions self.voiceMessagesAvailable = voiceMessagesAvailable + self.emojiStatus = emojiStatus self.peerIds = Set() @@ -189,6 +200,8 @@ public final class CachedUserData: CachedPeerData { self.premiumGiftOptions = decoder.decodeObjectArrayWithDecoderForKey("pgo") as [CachedPremiumGiftOption] self.voiceMessagesAvailable = decoder.decodeInt32ForKey("vma", orElse: 0) != 0 + self.emojiStatus = decoder.decode(PeerEmojiStatus.self, forKey: "emojiStatus") + self.peerIds = Set() var messageIds = Set() @@ -245,6 +258,12 @@ public final class CachedUserData: CachedPeerData { encoder.encodeObjectArray(self.premiumGiftOptions, forKey: "pgo") encoder.encodeInt32(self.voiceMessagesAvailable ? 1 : 0, forKey: "vma") + + if let emojiStatus = self.emojiStatus { + encoder.encode(emojiStatus, forKey: "emojiStatus") + } else { + encoder.encodeNil(forKey: "emojiStatus") + } } public func isEqual(to: CachedPeerData) -> Bool { @@ -258,71 +277,78 @@ public final class CachedUserData: CachedPeerData { if other.canPinMessages != self.canPinMessages { return false } + if other.emojiStatus != self.emojiStatus { + return false + } return other.about == self.about && other.botInfo == self.botInfo && self.peerStatusSettings == other.peerStatusSettings && self.isBlocked == other.isBlocked && self.commonGroupCount == other.commonGroupCount && self.voiceCallsAvailable == other.voiceCallsAvailable && self.videoCallsAvailable == other.videoCallsAvailable && self.callsPrivate == other.callsPrivate && self.hasScheduledMessages == other.hasScheduledMessages && self.autoremoveTimeout == other.autoremoveTimeout && self.themeEmoticon == other.themeEmoticon && self.photo == other.photo && self.premiumGiftOptions == other.premiumGiftOptions && self.voiceMessagesAvailable == other.voiceMessagesAvailable } public func withUpdatedAbout(_ about: String?) -> CachedUserData { - return CachedUserData(about: about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, emojiStatus: self.emojiStatus) } public func withUpdatedBotInfo(_ botInfo: BotInfo?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, emojiStatus: self.emojiStatus) } public func withUpdatedPeerStatusSettings(_ peerStatusSettings: PeerStatusSettings) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, emojiStatus: self.emojiStatus) } public func withUpdatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, emojiStatus: self.emojiStatus) } public func withUpdatedIsBlocked(_ isBlocked: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, emojiStatus: self.emojiStatus) } public func withUpdatedCommonGroupCount(_ commonGroupCount: Int32) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, emojiStatus: self.emojiStatus) } public func withUpdatedVoiceCallsAvailable(_ voiceCallsAvailable: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, emojiStatus: self.emojiStatus) } public func withUpdatedVideoCallsAvailable(_ videoCallsAvailable: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, emojiStatus: self.emojiStatus) } public func withUpdatedCallsPrivate(_ callsPrivate: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, emojiStatus: self.emojiStatus) } public func withUpdatedCanPinMessages(_ canPinMessages: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, emojiStatus: self.emojiStatus) } public func withUpdatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, emojiStatus: self.emojiStatus) } public func withUpdatedAutoremoveTimeout(_ autoremoveTimeout: CachedPeerAutoremoveTimeout) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, emojiStatus: self.emojiStatus) } public func withUpdatedThemeEmoticon(_ themeEmoticon: String?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, emojiStatus: self.emojiStatus) } public func withUpdatedPhoto(_ photo: TelegramMediaImage?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, emojiStatus: self.emojiStatus) } public func withUpdatedPremiumGiftOptions(_ premiumGiftOptions: [CachedPremiumGiftOption]) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, emojiStatus: self.emojiStatus) } public func withUpdatedVoiceMessagesAvailable(_ voiceMessagesAvailable: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: voiceMessagesAvailable, emojiStatus: self.emojiStatus) + } + + public func withUpdatedEmojiStatus(_ emojiStatus: PeerEmojiStatus?) -> CachedUserData { + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, emojiStatus: emojiStatus) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift index 4e732b6f3c..296be02831 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift @@ -53,5 +53,23 @@ public extension TelegramEngine { public func removeAccountPhoto(reference: TelegramMediaImageReference?) -> Signal { return _internal_removeAccountPhoto(network: self.account.network, reference: reference) } + + public func setEmojiStatus(file: TelegramMediaFile?) -> Signal { + let peerId = self.account.peerId + return self.account.postbox.transaction { transaction -> Void in + if let file = file { + transaction.storeMediaIfNotPresent(media: file) + } + transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in + guard let current = current as? CachedUserData else { + return current + } + return current.withUpdatedEmojiStatus(file.flatMap { file -> PeerEmojiStatus in + return PeerEmojiStatus(fileId: file.fileId.id) + }) + }) + } + |> ignoreValues + } } } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index be2389ee34..52fe5cee57 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -290,6 +290,8 @@ swift_library( "//submodules/TelegramUI/Components/ChatInputPanelContainer:ChatInputPanelContainer", "//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities", "//submodules/TelegramUI/Components/EmojiSuggestionsComponent:EmojiSuggestionsComponent", + "//submodules/TelegramUI/Components/EmojiStatusSelectionComponent:EmojiStatusSelectionComponent", + "//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent", "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", "//submodules/Media/ConvertOpusToAAC:ConvertOpusToAAC", "//submodules/Media/LocalAudioTranscription:LocalAudioTranscription", diff --git a/submodules/TelegramUI/Components/EmojiStatusComponent/BUILD b/submodules/TelegramUI/Components/EmojiStatusComponent/BUILD new file mode 100644 index 0000000000..824a18d505 --- /dev/null +++ b/submodules/TelegramUI/Components/EmojiStatusComponent/BUILD @@ -0,0 +1,28 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "EmojiStatusComponent", + module_name = "EmojiStatusComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Display:Display", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", + "//submodules/AccountContext:AccountContext", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/AppBundle:AppBundle", + "//submodules/TextFormat:TextFormat", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift new file mode 100644 index 0000000000..7dc2fc23f8 --- /dev/null +++ b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift @@ -0,0 +1,341 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Display +import AnimationCache +import MultiAnimationRenderer +import ComponentFlow +import AccountContext +import TelegramCore +import Postbox +import EmojiTextAttachmentView +import AppBundle +import TextFormat + +public final class EmojiStatusComponent: Component { + public typealias EnvironmentType = Empty + + 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 emojiStatus(status: PeerEmojiStatus, placeholderColor: UIColor) + } + + public let context: AccountContext + public let animationCache: AnimationCache + public let animationRenderer: MultiAnimationRenderer + public let content: Content + public let action: (() -> Void)? + public let longTapAction: (() -> Void)? + + public init( + context: AccountContext, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer, + content: Content, + action: (() -> Void)?, + longTapAction: (() -> Void)? + ) { + self.context = context + self.animationCache = animationCache + self.animationRenderer = animationRenderer + self.content = content + self.action = action + self.longTapAction = longTapAction + } + + public static func ==(lhs: EmojiStatusComponent, rhs: EmojiStatusComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.animationCache !== rhs.animationCache { + return false + } + if lhs.animationRenderer !== rhs.animationRenderer { + return false + } + if lhs.content != rhs.content { + return false + } + return true + } + + public final class View: UIView { + private weak var state: EmptyComponentState? + private var component: EmojiStatusComponent? + private var iconView: UIImageView? + private var animationLayer: InlineStickerItemLayer? + + private var emojiFile: TelegramMediaFile? + private var emojiFileDisposable: Disposable? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + self.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.emojiFileDisposable?.dispose() + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.component?.action?() + } + } + + @objc private func longPressGesture(_ recognizer: UITapGestureRecognizer) { + if case .began = recognizer.state { + self.component?.longTapAction?() + } + } + + func update(component: EmojiStatusComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.state = state + + var iconImage: UIImage? + var emojiFileId: Int64? + var emojiPlaceholderColor: UIColor? + + /* + if case .fake = credibilityIcon { + image = PresentationResourcesChatList.fakeIcon(presentationData.theme, strings: presentationData.strings, type: .regular) + } else if case .scam = credibilityIcon { + image = PresentationResourcesChatList.scamIcon(presentationData.theme, strings: presentationData.strings, type: .regular) + } else if case .verified = credibilityIcon { + if let backgroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconBackground"), let foregroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconForeground") { + image = generateImage(backgroundImage.size, contextGenerator: { size, context in + if let backgroundCgImage = backgroundImage.cgImage, let foregroundCgImage = foregroundImage.cgImage { + context.clear(CGRect(origin: CGPoint(), size: size)) + context.saveGState() + context.clip(to: CGRect(origin: .zero, size: size), mask: backgroundCgImage) + + context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.restoreGState() + + context.clip(to: CGRect(origin: .zero, size: size), mask: foregroundCgImage) + context.setFillColor(presentationData.theme.list.itemCheckColors.foregroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + } + }, opaque: false) + expandedImage = generateImage(backgroundImage.size, contextGenerator: { size, context in + if let backgroundCgImage = backgroundImage.cgImage, let foregroundCgImage = foregroundImage.cgImage { + context.clear(CGRect(origin: CGPoint(), size: size)) + context.saveGState() + context.clip(to: CGRect(origin: .zero, size: size), mask: backgroundCgImage) + context.setFillColor(UIColor(rgb: 0xffffff, alpha: 0.75).cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.restoreGState() + + context.clip(to: CGRect(origin: .zero, size: size), mask: foregroundCgImage) + context.setBlendMode(.clear) + context.fill(CGRect(origin: CGPoint(), size: size)) + } + }, opaque: false) + } else { + image = nil + } + } else if case .premium = credibilityIcon { + if let sourceImage = UIImage(bundleImageName: "Peer Info/PremiumIcon") { + image = generateImage(sourceImage.size, contextGenerator: { size, context in + if let cgImage = sourceImage.cgImage { + context.clear(CGRect(origin: CGPoint(), size: size)) + context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) + + context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + } + }, opaque: false) + expandedImage = generateImage(sourceImage.size, contextGenerator: { size, context in + if let cgImage = sourceImage.cgImage { + context.clear(CGRect(origin: CGPoint(), size: size)) + context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) + context.setFillColor(UIColor(rgb: 0xffffff, alpha: 0.75).cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + } + }, opaque: false) + } else { + image = nil + } + } + */ + + if self.component?.content != component.content { + switch component.content { + case .none: + iconImage = nil + case let .premium(color): + if let sourceImage = UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon") { + iconImage = generateImage(sourceImage.size, contextGenerator: { size, context in + if let cgImage = sourceImage.cgImage { + context.clear(CGRect(origin: CGPoint(), size: size)) + context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) + + context.setFillColor(color.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + } + }, opaque: false) + } else { + iconImage = nil + } + case let .verified(fillColor, foregroundColor): + if let backgroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconBackground"), let foregroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconForeground") { + iconImage = generateImage(backgroundImage.size, contextGenerator: { size, context in + if let backgroundCgImage = backgroundImage.cgImage, let foregroundCgImage = foregroundImage.cgImage { + context.clear(CGRect(origin: CGPoint(), size: size)) + context.saveGState() + context.clip(to: CGRect(origin: .zero, size: size), mask: backgroundCgImage) + + context.setFillColor(fillColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.restoreGState() + + context.clip(to: CGRect(origin: .zero, size: size), mask: foregroundCgImage) + context.setFillColor(foregroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + } + }, opaque: false) + } else { + iconImage = nil + } + case .fake: + iconImage = nil + case .scam: + iconImage = nil + case let .emojiStatus(emojiStatus, placeholderColor): + iconImage = nil + emojiFileId = emojiStatus.fileId + emojiPlaceholderColor = placeholderColor + + if case let .emojiStatus(previousEmojiStatus, _) = self.component?.content { + if previousEmojiStatus.fileId != emojiStatus.fileId { + self.emojiFileDisposable?.dispose() + self.emojiFileDisposable = nil + + self.emojiFile = nil + + if let animationLayer = self.animationLayer { + self.animationLayer = nil + animationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak animationLayer] _ in + animationLayer?.removeFromSuperlayer() + }) + animationLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } + } + } + } + } else { + iconImage = self.iconView?.image + if case let .emojiStatus(status, placeholderColor) = component.content { + emojiFileId = status.fileId + emojiPlaceholderColor = placeholderColor + } + } + + self.component = component + + var size = CGSize() + + if let iconImage = iconImage { + let iconView: UIImageView + if let current = self.iconView { + iconView = current + } else { + iconView = UIImageView() + self.iconView = iconView + self.addSubview(iconView) + + iconView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + iconView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + } + iconView.image = iconImage + size = iconImage.size.aspectFilled(availableSize) + iconView.frame = CGRect(origin: CGPoint(), size: size) + } else { + if let iconView = self.iconView { + self.iconView = nil + + iconView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak iconView] _ in + iconView?.removeFromSuperview() + }) + iconView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } + } + + if let emojiFileId = emojiFileId, let emojiPlaceholderColor = emojiPlaceholderColor { + size = availableSize + + if let emojiFile = self.emojiFile { + self.emojiFileDisposable?.dispose() + self.emojiFileDisposable = nil + + let animationLayer: InlineStickerItemLayer + if let current = self.animationLayer { + animationLayer = current + } else { + animationLayer = InlineStickerItemLayer( + context: component.context, + attemptSynchronousLoad: false, + emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: nil, fileId: emojiFile.fileId.id, file: emojiFile), + file: emojiFile, + cache: component.animationCache, + renderer: component.animationRenderer, + placeholderColor: emojiPlaceholderColor, + pointSize: CGSize(width: 32.0, height: 32.0) + ) + self.animationLayer = animationLayer + self.layer.addSublayer(animationLayer) + + animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + animationLayer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + } + animationLayer.frame = CGRect(origin: CGPoint(), size: size) + animationLayer.isVisibleForAnimations = true + } else { + if self.emojiFileDisposable == nil { + self.emojiFileDisposable = (component.context.engine.stickers.resolveInlineStickers(fileIds: [emojiFileId]) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + strongSelf.emojiFile = result[emojiFileId] + strongSelf.state?.updated(transition: .immediate) + }) + } + } + } else { + self.emojiFileDisposable?.dispose() + self.emojiFileDisposable = nil + + if let animationLayer = self.animationLayer { + self.animationLayer = nil + + animationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak animationLayer] _ in + animationLayer?.removeFromSuperlayer() + }) + animationLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } + } + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD new file mode 100644 index 0000000000..f5c23070fc --- /dev/null +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD @@ -0,0 +1,28 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "EmojiStatusSelectionComponent", + module_name = "EmojiStatusSelectionComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard", + "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", + "//submodules/Components/PagerComponent:PagerComponent", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/AccountContext:AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift new file mode 100644 index 0000000000..7a36978560 --- /dev/null +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -0,0 +1,437 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import AnimationCache +import MultiAnimationRenderer +import EntityKeyboard +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import PagerComponent + +public final class EmojiStatusSelectionComponent: Component { + public typealias EnvironmentType = Empty + + public let theme: PresentationTheme + public let strings: PresentationStrings + public let deviceMetrics: DeviceMetrics + public let emojiContent: EmojiPagerContentComponent + + public init( + theme: PresentationTheme, + strings: PresentationStrings, + deviceMetrics: DeviceMetrics, + emojiContent: EmojiPagerContentComponent + ) { + self.theme = theme + self.strings = strings + self.deviceMetrics = deviceMetrics + self.emojiContent = emojiContent + } + + public static func ==(lhs: EmojiStatusSelectionComponent, rhs: EmojiStatusSelectionComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings != rhs.strings { + return false + } + if lhs.deviceMetrics != rhs.deviceMetrics { + return false + } + if lhs.emojiContent != rhs.emojiContent { + return false + } + return true + } + + public final class View: UIView { + private let keyboardView: ComponentView + private let panelHostView: PagerExternalTopPanelContainer + private let panelBackgroundView: BlurredBackgroundView + private let panelSeparatorView: UIView + + private var component: EmojiStatusSelectionComponent? + + override init(frame: CGRect) { + self.keyboardView = ComponentView() + self.panelHostView = PagerExternalTopPanelContainer() + self.panelBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + self.panelSeparatorView = UIView() + + super.init(frame: frame) + + self.clipsToBounds = true + self.layer.cornerRadius = 24.0 + + self.addSubview(self.panelBackgroundView) + self.addSubview(self.panelSeparatorView) + self.addSubview(self.panelHostView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: EmojiStatusSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + if self.component?.theme !== component.theme { + self.backgroundColor = component.theme.list.plainBackgroundColor + self.panelBackgroundView.updateColor(color: component.theme.list.plainBackgroundColor.withMultipliedAlpha(0.85), transition: .immediate) + self.panelSeparatorView.backgroundColor = component.theme.list.itemPlainSeparatorColor.withMultipliedAlpha(0.5) + } + + self.component = component + + let keyboardSize = self.keyboardView.update( + transition: transition, + component: AnyComponent(EntityKeyboardComponent( + theme: component.theme, + strings: component.strings, + containerInsets: UIEdgeInsets(top: 41.0 - 34.0, left: 0.0, bottom: 0.0, right: 0.0), + topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0), + emojiContent: component.emojiContent, + stickerContent: nil, + gifContent: nil, + hasRecentGifs: false, + availableGifSearchEmojies: [], + defaultToEmojiTab: true, + externalTopPanelContainer: self.panelHostView, + topPanelExtensionUpdated: { _, _ in }, + hideInputUpdated: { _, _, _ in }, + switchToTextInput: {}, + switchToGifSubject: { _ in }, + makeSearchContainerNode: { _ in return nil }, + deviceMetrics: component.deviceMetrics, + hiddenInputHeight: 0.0, + displayBottomPanel: false, + isExpanded: false + )), + environment: {}, + containerSize: availableSize + ) + if let keyboardComponentView = self.keyboardView.view { + if keyboardComponentView.superview == nil { + self.insertSubview(keyboardComponentView, at: 0) + } + transition.setFrame(view: keyboardComponentView, frame: CGRect(origin: CGPoint(), size: keyboardSize)) + transition.setFrame(view: self.panelHostView, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 - 34.0), size: CGSize(width: keyboardSize.width, height: 0.0))) + + transition.setFrame(view: self.panelBackgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: keyboardSize.width, height: 41.0))) + self.panelBackgroundView.update(size: self.panelBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition) + + transition.setFrame(view: self.panelSeparatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0), size: CGSize(width: keyboardSize.width, height: UIScreenPixel))) + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class EmojiStatusSelectionController: ViewController { + private final class Node: ViewControllerTracingNode { + private weak var controller: EmojiStatusSelectionController? + private let context: AccountContext + private weak var sourceView: UIView? + private var globalSourceRect: CGRect? + + private let componentHost: ComponentView + private let componentShadowLayer: SimpleLayer + + private let cloudLayer0: SimpleLayer + private let cloudShadowLayer0: SimpleLayer + private let cloudLayer1: SimpleLayer + private let cloudShadowLayer1: SimpleLayer + + private var presentationData: PresentationData + private var validLayout: ContainerViewLayout? + + private var emojiContentDisposable: Disposable? + private var emojiContent: EmojiPagerContentComponent? + + private var isDismissed: Bool = false + + init(controller: EmojiStatusSelectionController, context: AccountContext, sourceView: UIView?, emojiContent: Signal) { + self.controller = controller + self.context = context + + if let sourceView = sourceView { + self.globalSourceRect = sourceView.convert(sourceView.bounds, to: nil) + } + + self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + self.componentHost = ComponentView() + self.componentShadowLayer = SimpleLayer() + self.componentShadowLayer.shadowOpacity = 0.15 + self.componentShadowLayer.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor + self.componentShadowLayer.shadowOffset = CGSize(width: 0.0, height: 2.0) + self.componentShadowLayer.shadowRadius = 15.0 + + self.cloudLayer0 = SimpleLayer() + self.cloudShadowLayer0 = SimpleLayer() + self.cloudShadowLayer0.shadowOpacity = 0.15 + self.cloudShadowLayer0.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor + self.cloudShadowLayer0.shadowOffset = CGSize(width: 0.0, height: 2.0) + self.cloudShadowLayer0.shadowRadius = 15.0 + + self.cloudLayer1 = SimpleLayer() + self.cloudShadowLayer1 = SimpleLayer() + self.cloudShadowLayer1.shadowOpacity = 0.15 + self.cloudShadowLayer1.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor + self.cloudShadowLayer1.shadowOffset = CGSize(width: 0.0, height: 2.0) + self.cloudShadowLayer1.shadowRadius = 15.0 + + super.init() + + self.layer.addSublayer(self.componentShadowLayer) + self.layer.addSublayer(self.cloudShadowLayer0) + self.layer.addSublayer(self.cloudShadowLayer1) + + self.layer.addSublayer(self.cloudLayer0) + self.layer.addSublayer(self.cloudLayer1) + + self.emojiContentDisposable = (emojiContent + |> deliverOnMainQueue).start(next: { [weak self] emojiContent in + guard let strongSelf = self else { + return + } + strongSelf.controller?._ready.set(.single(true)) + strongSelf.emojiContent = emojiContent + + emojiContent.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( + performItemAction: { _, item, _, _, _ in + guard let strongSelf = self else { + return + } + strongSelf.applyItem(item: item) + }, + deleteBackwards: { + }, + openStickerSettings: { + }, + openFeatured: { + }, + addGroupAction: { groupId, isPremiumLocked in + + }, + clearGroup: { groupId in + }, + pushController: { c in + }, + presentController: { c in + }, + presentGlobalOverlayController: { c in + }, + navigationController: { + return nil + }, + sendSticker: nil, + chatPeerId: nil + ) + + strongSelf.refreshLayout(transition: .immediate) + }) + } + + deinit { + self.emojiContentDisposable?.dispose() + } + + private func refreshLayout(transition: Transition) { + guard let layout = self.validLayout else { + return + } + self.containerLayoutUpdated(layout: layout, transition: transition) + } + + func animateOut(completion: @escaping () -> Void) { + self.componentShadowLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + self.componentHost.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + completion() + }) + + self.cloudLayer0.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + self.cloudShadowLayer0.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + self.cloudLayer1.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + self.cloudShadowLayer1.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + } + + func containerLayoutUpdated(layout: ContainerViewLayout, transition: Transition) { + self.validLayout = layout + + guard let emojiContent = self.emojiContent else { + return + } + + self.cloudLayer0.backgroundColor = self.presentationData.theme.list.plainBackgroundColor.cgColor + self.cloudLayer1.backgroundColor = self.presentationData.theme.list.plainBackgroundColor.cgColor + + let sideInset: CGFloat = 16.0 + + let componentSize = self.componentHost.update( + transition: transition, + component: AnyComponent(EmojiStatusSelectionComponent( + theme: self.presentationData.theme, + strings: self.presentationData.strings, + deviceMetrics: layout.deviceMetrics, + emojiContent: emojiContent + )), + environment: {}, + containerSize: CGSize(width: layout.size.width - sideInset * 2.0, height: min(300.0, layout.size.height)) + ) + if let componentView = self.componentHost.view { + var animateIn = false + if componentView.superview == nil { + self.view.addSubview(componentView) + animateIn = true + } + + let sourceOrigin: CGPoint + if let sourceView = self.sourceView { + let sourceRect = sourceView.convert(sourceView.bounds, to: self.view) + sourceOrigin = CGPoint(x: sourceRect.midX, y: sourceRect.maxY) + } else if let globalSourceRect = self.globalSourceRect { + let sourceRect = self.view.convert(globalSourceRect, from: nil) + sourceOrigin = CGPoint(x: sourceRect.midX, y: sourceRect.maxY) + } else { + sourceOrigin = CGPoint(x: layout.size.width / 2.0, y: floor(layout.size.height / 2.0 - componentSize.height)) + } + + let componentFrame = CGRect(origin: CGPoint(x: sideInset, y: sourceOrigin.y + 8.0), size: componentSize) + + if self.componentShadowLayer.bounds.size != componentFrame.size { + let componentShadowPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: componentFrame.size), cornerRadius: 24.0).cgPath + self.componentShadowLayer.shadowPath = componentShadowPath + } + transition.setFrame(layer: self.componentShadowLayer, frame: componentFrame) + + let cloudOffset0: CGFloat = 30.0 + let cloudSize0: CGFloat = 16.0 + let cloudFrame0 = CGRect(origin: CGPoint(x: floor(sourceOrigin.x + cloudOffset0 - cloudSize0 / 2.0), y: componentFrame.minY - cloudSize0 / 2.0), size: CGSize(width: cloudSize0, height: cloudSize0)) + transition.setFrame(layer: self.cloudLayer0, frame: cloudFrame0) + if self.cloudShadowLayer0.bounds.size != cloudFrame0.size { + let cloudShadowPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: cloudFrame0.size), cornerRadius: 24.0).cgPath + self.cloudShadowLayer0.shadowPath = cloudShadowPath + } + transition.setFrame(layer: self.cloudShadowLayer0, frame: cloudFrame0) + transition.setCornerRadius(layer: self.cloudLayer0, cornerRadius: cloudFrame0.width / 2.0) + + let cloudOffset1 = CGPoint(x: -9.0, y: -14.0) + let cloudSize1: CGFloat = 8.0 + let cloudFrame1 = CGRect(origin: CGPoint(x: floor(cloudFrame0.midX + cloudOffset1.x - cloudSize1 / 2.0), y: floor(cloudFrame0.midY + cloudOffset1.y - cloudSize1 / 2.0)), size: CGSize(width: cloudSize1, height: cloudSize1)) + transition.setFrame(layer: self.cloudLayer1, frame: cloudFrame1) + if self.cloudShadowLayer1.bounds.size != cloudFrame1.size { + let cloudShadowPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: cloudFrame1.size), cornerRadius: 24.0).cgPath + self.cloudShadowLayer1.shadowPath = cloudShadowPath + } + transition.setFrame(layer: self.cloudShadowLayer1, frame: cloudFrame1) + transition.setCornerRadius(layer: self.cloudLayer1, cornerRadius: cloudFrame1.width / 2.0) + + transition.setFrame(view: componentView, frame: componentFrame) + + if animateIn { + self.allowsGroupOpacity = true + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak self] _ in + self?.allowsGroupOpacity = false + }) + + //componentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.28) + componentView.layer.animateScale(from: (componentView.bounds.width - 10.0) / componentView.bounds.width, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + + //self.cloudShadowLayer0.animateAlpha(from: 0.0, to: 1.0, duration: 0.28) + //self.cloudLayer0.animateAlpha(from: 0.0, to: 1.0, duration: 0.28) + self.cloudLayer0.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + self.cloudShadowLayer0.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + + //self.cloudShadowLayer1.animateAlpha(from: 0.0, to: 1.0, duration: 0.28) + //self.cloudLayer1.animateAlpha(from: 0.0, to: 1.0, duration: 0.28) + self.cloudLayer1.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + self.cloudShadowLayer1.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + } + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let result = super.hitTest(point, with: event) { + if self.isDismissed { + return self.view + } + + if result === self.view { + self.isDismissed = true + self.controller?.dismiss() + } + + return result + } + return nil + } + + private func applyItem(item: EmojiPagerContentComponent.Item?) { + let _ = (self.context.engine.accountData.setEmojiStatus(file: item?.itemFile) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.controller?.dismiss() + }) + } + } + + private let context: AccountContext + private weak var sourceView: UIView? + private let emojiContent: Signal + + fileprivate let _ready = Promise() + override public var ready: Promise { + return self._ready + } + + public init(context: AccountContext, sourceView: UIView, emojiContent: Signal) { + self.context = context + self.sourceView = sourceView + self.emojiContent = emojiContent + + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Ignore + } + + required public init(coder: NSCoder) { + preconditionFailure() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + } + + override public func dismiss(completion: (() -> Void)? = nil) { + (self.displayNode as! Node).animateOut(completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + completion?() + }) + } + + override public func loadDisplayNode() { + self.displayNode = Node(controller: self, context: self.context, sourceView: self.sourceView, emojiContent: self.emojiContent) + + super.displayNodeDidLoad() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition)) + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 990e8efccf..bc83b1d16c 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -1619,6 +1619,7 @@ public final class EmojiPagerContentComponent: Component { var width: CGFloat var headerInsets: UIEdgeInsets var itemInsets: UIEdgeInsets + var curveNearBounds: Bool var itemGroupLayouts: [ItemGroupLayout] var itemDefaultHeaderHeight: CGFloat var itemFeaturedHeaderHeight: CGFloat @@ -1642,6 +1643,8 @@ public final class EmojiPagerContentComponent: Component { self.premiumButtonInset = 6.0 self.premiumButtonHeight = 50.0 + self.curveNearBounds = containerInsets.bottom <= 9.0 + let minItemsPerRow: Int let minSpacing: CGFloat switch layoutType { @@ -2258,6 +2261,7 @@ public final class EmojiPagerContentComponent: Component { private var vibrancyEffectView: UIVisualEffectView? private let mirrorContentScrollView: UIView private let scrollView: ContentScrollView + private var scrollGradientLayer: SimpleGradientLayer? private let boundsChangeTrackerLayer = SimpleLayer() private var effectiveVisibleSize: CGSize = CGSize() @@ -3562,6 +3566,29 @@ public final class EmojiPagerContentComponent: Component { let itemPosition = CGPoint(x: itemFrame.midX, y: itemFrame.midY) itemTransition.setPosition(layer: itemLayer, position: itemPosition) + if itemLayout.curveNearBounds { + let fractionLength: CGFloat = itemFrame.height + let rotationX: CGFloat = 1.0 - max(0.0, min(1.0, (effectiveVisibleBounds.maxY + 4.0 + itemFrame.height / 2.0 - itemPosition.y) / fractionLength)) + if rotationX.isZero { + itemTransition.setTransform(layer: itemLayer, transform: CATransform3DIdentity) + } else { + var transform = CATransform3DIdentity + + let centralOffset = itemFrame.midX - effectiveVisibleBounds.width / 2.0 + transform = CATransform3DTranslate(transform, centralOffset * 2.0, 0.0, 0.0) + + var projectionMatrix = CATransform3DIdentity + projectionMatrix.m34 = -1.0 / 500.0 + transform = CATransform3DConcat(projectionMatrix, transform) + + transform = CATransform3DTranslate(transform, -centralOffset * 2.0, 0.0, 0.0) + + transform = CATransform3DTranslate(transform, 0.0, -rotationX * itemFrame.height * 0.5, 0.0) + transform = CATransform3DRotate(transform, CGFloat.pi * 0.5 * rotationX, 1.0, 0.0, 0.0) + itemTransition.setTransform(layer: itemLayer, transform: transform) + } + } + var badge: ItemLayer.Badge? if itemGroup.displayPremiumBadges, let file = item.itemFile, file.isPremiumSticker { badge = .premium @@ -3764,6 +3791,44 @@ public final class EmojiPagerContentComponent: Component { self.updateShimmerIfNeeded() } + if itemLayout.curveNearBounds { + let scrollGradientLayer: SimpleGradientLayer + if let current = self.scrollGradientLayer { + scrollGradientLayer = current + } else { + scrollGradientLayer = SimpleGradientLayer() + self.scrollGradientLayer = scrollGradientLayer + + var locations: [NSNumber] = [] + var colors: [CGColor] = [] + let numStops = 6 + for i in 0 ..< numStops { + let step = CGFloat(i) / CGFloat(numStops - 1) + locations.append(step as NSNumber) + colors.append(keyboardChildEnvironment.theme.list.plainBackgroundColor.withAlphaComponent(step * step).cgColor) + } + + scrollGradientLayer.locations = locations + scrollGradientLayer.colors = colors + scrollGradientLayer.type = .axial + + self.layer.addSublayer(scrollGradientLayer) + } + + let gradientHeight: CGFloat = 20.0 + transition.setFrame(layer: scrollGradientLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: effectiveVisibleBounds.height - gradientHeight), size: CGSize(width: effectiveVisibleBounds.width, height: gradientHeight))) + //let fractionLength: CGFloat = 20.0 + //transition.setAlpha(layer: scrollGradientLayer, alpha: max(0.0, min(1.0, (itemLayout.contentSize.height - effectiveVisibleBounds.maxY) / fractionLength))) + + scrollGradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0) + scrollGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0) + } else { + if let scrollGradientLayer = self.scrollGradientLayer { + self.scrollGradientLayer = nil + scrollGradientLayer.removeFromSuperlayer() + } + } + if let topVisibleGroupId = topVisibleGroupId { self.activeItemUpdated?.invoke((topVisibleGroupId, topVisibleSubgroupId, .immediate)) } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index ef138bfe6b..1741051272 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -6,7 +6,6 @@ import PagerComponent import TelegramPresentationData import TelegramCore import Postbox -import BlurredBackgroundComponent import BundleIconComponent import AudioToolbox import SwiftSignalKit @@ -83,6 +82,7 @@ public final class EntityKeyboardComponent: Component { public let theme: PresentationTheme public let strings: PresentationStrings public let containerInsets: UIEdgeInsets + public let topPanelInsets: UIEdgeInsets public let emojiContent: EmojiPagerContentComponent public let stickerContent: EmojiPagerContentComponent? public let gifContent: GifPagerContentComponent? @@ -97,12 +97,14 @@ public final class EntityKeyboardComponent: Component { public let makeSearchContainerNode: (EntitySearchContentType) -> EntitySearchContainerNode? public let deviceMetrics: DeviceMetrics public let hiddenInputHeight: CGFloat + public let displayBottomPanel: Bool public let isExpanded: Bool public init( theme: PresentationTheme, strings: PresentationStrings, containerInsets: UIEdgeInsets, + topPanelInsets: UIEdgeInsets, emojiContent: EmojiPagerContentComponent, stickerContent: EmojiPagerContentComponent?, gifContent: GifPagerContentComponent?, @@ -117,11 +119,13 @@ public final class EntityKeyboardComponent: Component { makeSearchContainerNode: @escaping (EntitySearchContentType) -> EntitySearchContainerNode?, deviceMetrics: DeviceMetrics, hiddenInputHeight: CGFloat, + displayBottomPanel: Bool, isExpanded: Bool ) { self.theme = theme self.strings = strings self.containerInsets = containerInsets + self.topPanelInsets = topPanelInsets self.emojiContent = emojiContent self.stickerContent = stickerContent self.gifContent = gifContent @@ -136,6 +140,7 @@ public final class EntityKeyboardComponent: Component { self.makeSearchContainerNode = makeSearchContainerNode self.deviceMetrics = deviceMetrics self.hiddenInputHeight = hiddenInputHeight + self.displayBottomPanel = displayBottomPanel self.isExpanded = isExpanded } @@ -149,6 +154,9 @@ public final class EntityKeyboardComponent: Component { if lhs.containerInsets != rhs.containerInsets { return false } + if lhs.topPanelInsets != rhs.topPanelInsets { + return false + } if lhs.emojiContent != rhs.emojiContent { return false } @@ -176,6 +184,9 @@ public final class EntityKeyboardComponent: Component { if lhs.hiddenInputHeight != rhs.hiddenInputHeight { return false } + if lhs.displayBottomPanel != rhs.displayBottomPanel { + return false + } if lhs.isExpanded != rhs.isExpanded { return false } @@ -247,6 +258,7 @@ public final class EntityKeyboardComponent: Component { content: AnyComponent(EntityKeyboardIconTopPanelComponent( icon: .recent, theme: component.theme, + useAccentColor: !component.displayBottomPanel, title: component.strings.Stickers_Recent, pressed: { [weak self] in self?.component?.switchToGifSubject(.recent) @@ -260,6 +272,7 @@ public final class EntityKeyboardComponent: Component { content: AnyComponent(EntityKeyboardIconTopPanelComponent( icon: .trending, theme: component.theme, + useAccentColor: !component.displayBottomPanel, title: component.strings.Stickers_Trending, pressed: { [weak self] in self?.component?.switchToGifSubject(.trending) @@ -297,7 +310,7 @@ public final class EntityKeyboardComponent: Component { contentTopPanels.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(EntityKeyboardTopPanelComponent( theme: component.theme, items: topGifItems, - containerSideInset: component.containerInsets.left, + containerSideInset: component.containerInsets.left + component.topPanelInsets.left, forceActiveItemId: defaultActiveGifItemId, activeContentItemIdUpdated: gifsContentItemIdUpdated, reorderItems: { _ in @@ -325,6 +338,7 @@ public final class EntityKeyboardComponent: Component { content: AnyComponent(EntityKeyboardIconTopPanelComponent( icon: .featured, theme: component.theme, + useAccentColor: !component.displayBottomPanel, title: component.strings.Stickers_Trending, pressed: { [weak self] in self?.component?.stickerContent?.inputInteractionHolder.inputInteraction?.openFeatured() @@ -368,6 +382,7 @@ public final class EntityKeyboardComponent: Component { content: AnyComponent(EntityKeyboardIconTopPanelComponent( icon: icon, theme: component.theme, + useAccentColor: !component.displayBottomPanel, title: title, pressed: { [weak self] in self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil) @@ -404,7 +419,7 @@ public final class EntityKeyboardComponent: Component { contentTopPanels.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardTopPanelComponent( theme: component.theme, items: topStickerItems, - containerSideInset: component.containerInsets.left, + containerSideInset: component.containerInsets.left + component.topPanelInsets.left, defaultActiveItemId: stickerContent.itemGroups.first?.groupId, activeContentItemIdUpdated: stickersContentItemIdUpdated, reorderItems: { [weak self] items in @@ -457,6 +472,7 @@ public final class EntityKeyboardComponent: Component { content: AnyComponent(EntityKeyboardIconTopPanelComponent( icon: icon, theme: component.theme, + useAccentColor: !component.displayBottomPanel, title: title, pressed: { [weak self] in self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: nil) @@ -506,7 +522,7 @@ public final class EntityKeyboardComponent: Component { contentTopPanels.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardTopPanelComponent( theme: component.theme, items: topEmojiItems, - containerSideInset: component.containerInsets.left, + containerSideInset: component.containerInsets.left + component.topPanelInsets.left, activeContentItemIdUpdated: emojiContentItemIdUpdated, reorderItems: { [weak self] items in guard let strongSelf = self else { @@ -571,14 +587,14 @@ public final class EntityKeyboardComponent: Component { displayBackground: component.externalTopPanelContainer == nil )), externalTopPanelContainer: component.externalTopPanelContainer, - bottomPanel: AnyComponent(EntityKeyboardBottomPanelComponent( + bottomPanel: component.displayBottomPanel ? AnyComponent(EntityKeyboardBottomPanelComponent( theme: component.theme, containerInsets: component.containerInsets, deleteBackwards: { [weak self] in self?.component?.emojiContent.inputInteractionHolder.inputInteraction?.deleteBackwards() AudioServicesPlaySystemSound(0x451) } - )), + )) : nil, panelStateUpdated: { [weak self] panelState, transition in guard let strongSelf = self else { return diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index caf0c2a867..e77f6d027f 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -256,17 +256,20 @@ final class EntityKeyboardIconTopPanelComponent: Component { let icon: Icon let theme: PresentationTheme + let useAccentColor: Bool let title: String let pressed: () -> Void init( icon: Icon, theme: PresentationTheme, + useAccentColor: Bool, title: String, pressed: @escaping () -> Void ) { self.icon = icon self.theme = theme + self.useAccentColor = useAccentColor self.title = title self.pressed = pressed } @@ -278,6 +281,9 @@ final class EntityKeyboardIconTopPanelComponent: Component { if lhs.theme !== rhs.theme { return false } + if lhs.useAccentColor != rhs.useAccentColor { + return false + } if lhs.title != rhs.title { return false } @@ -352,7 +358,16 @@ final class EntityKeyboardIconTopPanelComponent: Component { self.component = component - let color = itemEnvironment.isHighlighted ? component.theme.chat.inputMediaPanel.panelHighlightedIconColor : component.theme.chat.inputMediaPanel.panelIconColor + let color: UIColor + if itemEnvironment.isHighlighted { + if component.useAccentColor { + color = component.theme.list.itemAccentColor + } else { + color = component.theme.chat.inputMediaPanel.panelHighlightedIconColor + } + } else { + color = component.theme.chat.inputMediaPanel.panelIconColor + } if self.iconView.tintColor != color { if !transition.animation.isImmediate { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 9d4514d4a0..9715ea4739 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -3788,7 +3788,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return true } - self.chatTitleView = ChatTitleView(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder) + self.chatTitleView = ChatTitleView(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: controllerInteraction.presentationContext.animationCache, animationRenderer: controllerInteraction.presentationContext.animationRenderer) self.navigationItem.titleView = self.chatTitleView self.chatTitleView?.pressed = { [weak self] in if let strongSelf = self { diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index 7f9d585bfb..128780c393 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -103,7 +103,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { return hasPremium } - static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool, areCustomEmojiEnabled: Bool, chatPeerId: EnginePeer.Id?) -> Signal { + static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool, areUnicodeEmojiEnabled: Bool, areCustomEmojiEnabled: Bool, chatPeerId: EnginePeer.Id?) -> Signal { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let isPremiumDisabled = premiumConfiguration.isPremiumDisabled @@ -178,21 +178,23 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } } - for (subgroupId, list) in staticEmojiMapping { - let groupId: AnyHashable = "static" - for emojiString in list { - let resultItem = EmojiPagerContentComponent.Item( - animationData: nil, - itemFile: nil, - staticEmoji: emojiString, - subgroupId: subgroupId.rawValue - ) - - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitleEmoji, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) + if areUnicodeEmojiEnabled { + for (subgroupId, list) in staticEmojiMapping { + let groupId: AnyHashable = "static" + for emojiString in list { + let resultItem = EmojiPagerContentComponent.Item( + animationData: nil, + itemFile: nil, + staticEmoji: emojiString, + subgroupId: subgroupId.rawValue + ) + + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitleEmoji, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) + } } } } @@ -378,7 +380,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { animationRenderer = MultiAnimationRendererImpl() //} - let emojiItems = emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: chatPeerId) + let emojiItems = emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: chatPeerId) let stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks] let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers] @@ -1759,6 +1761,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { theme: interfaceState.theme, strings: interfaceState.strings, containerInsets: UIEdgeInsets(top: 0.0, left: leftInset, bottom: bottomInset, right: rightInset), + topPanelInsets: UIEdgeInsets(), emojiContent: self.currentInputData.emoji, stickerContent: stickerContent, gifContent: gifContent?.component, @@ -1822,6 +1825,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { }, deviceMetrics: deviceMetrics, hiddenInputHeight: hiddenInputHeight, + displayBottomPanel: true, isExpanded: isExpanded )), environment: {}, @@ -2270,7 +2274,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV let semaphore = DispatchSemaphore(value: 0) var emojiComponent: EmojiPagerContentComponent? - let _ = ChatEntityKeyboardInputNode.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil).start(next: { value in + let _ = ChatEntityKeyboardInputNode.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil).start(next: { value in emojiComponent = value semaphore.signal() }) @@ -2285,7 +2289,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV gifs: nil, availableGifSearchEmojies: [] ), - updatedInputData: ChatEntityKeyboardInputNode.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in + updatedInputData: ChatEntityKeyboardInputNode.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in return ChatEntityKeyboardInputNode.InputData( emoji: emojiComponent, stickers: nil, diff --git a/submodules/TelegramUI/Sources/ChatTitleView.swift b/submodules/TelegramUI/Sources/ChatTitleView.swift index 7261c51428..034f5510a9 100644 --- a/submodules/TelegramUI/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Sources/ChatTitleView.swift @@ -17,6 +17,10 @@ import PhoneNumberFormat import ChatTitleActivityNode import AnimatedCountLabelNode import AccountContext +import ComponentFlow +import EmojiStatusComponent +import AnimationCache +import MultiAnimationRenderer private let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers]) private let subtitleFont = Font.regular(13.0) @@ -38,12 +42,13 @@ private enum ChatTitleIcon { case mute } -private enum ChatTitleCredibilityIcon { +private enum ChatTitleCredibilityIcon: Equatable { case none case fake case scam case verified case premium + case emojiStatus(PeerEmojiStatus) } final class ChatTitleView: UIView, NavigationBarTitleView { @@ -54,12 +59,14 @@ final class ChatTitleView: UIView, NavigationBarTitleView { private var strings: PresentationStrings private var dateTimeFormat: PresentationDateTimeFormat private var nameDisplayOrder: PresentationPersonNameOrder + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer private let contentContainer: ASDisplayNode let titleNode: ImmediateAnimatedCountLabelNode let titleLeftIconNode: ASImageNode let titleRightIconNode: ASImageNode - let titleCredibilityIconNode: ASImageNode + let titleCredibilityIconView: ComponentHostView let activityNode: ChatTitleActivityNode private let button: HighlightTrackingButtonNode @@ -142,7 +149,9 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } if peer.id != self.context.account.peerId { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) - if peer.isFake { + if let _ = peerView.cachedData as? CachedUserData, peer.isPremium && !premiumConfiguration.isPremiumDisabled, "".isEmpty { + titleCredibilityIcon = .emojiStatus(PeerEmojiStatus(fileId: 5431449001532594346)) + } else if peer.isFake { titleCredibilityIcon = .fake } else if peer.isScam { titleCredibilityIcon = .scam @@ -261,7 +270,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if titleCredibilityIcon != self.titleCredibilityIcon { self.titleCredibilityIcon = titleCredibilityIcon - switch titleCredibilityIcon { + /*switch titleCredibilityIcon { case .none: self.titleCredibilityIconNode.image = nil case .fake: @@ -272,7 +281,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.titleCredibilityIconNode.image = PresentationResourcesChatList.verifiedIcon(titleTheme) case .premium: self.titleCredibilityIconNode.image = PresentationResourcesChatList.premiumIcon(titleTheme) - } + }*/ updated = true } @@ -543,12 +552,14 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } } - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder) { + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) { self.context = context self.theme = theme self.strings = strings self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder + self.animationCache = animationCache + self.animationRenderer = animationRenderer self.contentContainer = ASDisplayNode() @@ -564,10 +575,8 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.titleRightIconNode.displayWithoutProcessing = true self.titleRightIconNode.displaysAsynchronously = false - self.titleCredibilityIconNode = ASImageNode() - self.titleCredibilityIconNode.isLayerBacked = true - self.titleCredibilityIconNode.displayWithoutProcessing = true - self.titleCredibilityIconNode.displaysAsynchronously = false + self.titleCredibilityIconView = ComponentHostView() + self.titleCredibilityIconView.isUserInteractionEnabled = false self.activityNode = ChatTitleActivityNode() self.button = HighlightTrackingButtonNode() @@ -592,13 +601,14 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if highlighted { strongSelf.titleNode.layer.removeAnimation(forKey: "opacity") strongSelf.activityNode.layer.removeAnimation(forKey: "opacity") - strongSelf.titleCredibilityIconNode.layer.removeAnimation(forKey: "opacity") + strongSelf.titleCredibilityIconView.layer.removeAnimation(forKey: "opacity") strongSelf.titleNode.alpha = 0.4 strongSelf.activityNode.alpha = 0.4 + strongSelf.titleCredibilityIconView.alpha = 0.4 } else { strongSelf.titleNode.alpha = 1.0 strongSelf.activityNode.alpha = 1.0 - strongSelf.titleCredibilityIconNode.alpha = 1.0 + strongSelf.titleCredibilityIconView.alpha = 1.0 strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) strongSelf.activityNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } @@ -653,13 +663,43 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.titleLeftIconNode.removeFromSupernode() } - if let image = self.titleCredibilityIconNode.image { - if self.titleCredibilityIconNode.supernode == nil { - self.titleNode.addSubnode(self.titleCredibilityIconNode) + let titleCredibilityContent: EmojiStatusComponent.Content + switch self.titleCredibilityIcon { + case .none: + titleCredibilityContent = .none + case .premium: + titleCredibilityContent = .premium(color: self.theme.list.itemAccentColor) + case .verified: + titleCredibilityContent = .verified(fillColor: self.theme.list.itemCheckColors.fillColor, foregroundColor: self.theme.list.itemCheckColors.foregroundColor) + case .fake: + titleCredibilityContent = .fake(color: self.theme.chat.message.incoming.scamColor) + case .scam: + titleCredibilityContent = .scam(color: self.theme.chat.message.incoming.scamColor) + case let .emojiStatus(emojiStatus): + titleCredibilityContent = .emojiStatus(status: emojiStatus, placeholderColor: self.theme.list.mediaPlaceholderColor) + } + + let titleCredibilitySize = self.titleCredibilityIconView.update( + transition: .immediate, + component: AnyComponent(EmojiStatusComponent( + context: self.context, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, + content: titleCredibilityContent, + action: nil, + longTapAction: nil + )), + environment: {}, + containerSize: CGSize(width: 20.0, height: 20.0) + ) + + if self.titleCredibilityIcon != .none { + self.titleNode.view.addSubview(self.titleCredibilityIconView) + credibilityIconWidth = titleCredibilitySize.width + 3.0 + } else { + if self.titleCredibilityIconView.superview != nil { + self.titleCredibilityIconView.removeFromSuperview() } - credibilityIconWidth = image.size.width + 3.0 - } else if self.titleCredibilityIconNode.supernode != nil { - self.titleCredibilityIconNode.removeFromSupernode() } if let image = self.titleRightIconNode.image { @@ -705,13 +745,9 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if let image = self.titleLeftIconNode.image { self.titleLeftIconNode.frame = CGRect(origin: CGPoint(x: -image.size.width - 3.0 - UIScreenPixel, y: 4.0), size: image.size) } - if let image = self.titleCredibilityIconNode.image { - var originY: CGFloat = 3.0 - if [.fake, .scam].contains(self.titleCredibilityIcon) { - originY = 2.0 - } - self.titleCredibilityIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.width - image.size.width, y: originY), size: image.size) - } + + self.titleCredibilityIconView.frame = CGRect(origin: CGPoint(x: titleFrame.width - titleCredibilitySize.width, y: floor((titleFrame.height - titleCredibilitySize.height) / 2.0)), size: titleCredibilitySize) + if let image = self.titleRightIconNode.image { self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.width + 3.0 + UIScreenPixel, y: 6.0), size: image.size) } @@ -729,13 +765,9 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if let image = self.titleLeftIconNode.image { self.titleLeftIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.minY + 4.0), size: image.size) } - if let image = self.titleCredibilityIconNode.image { - var originY: CGFloat = titleFrame.minY + 7.0 - if [.fake, .scam].contains(self.titleCredibilityIcon) { - originY = titleFrame.minY + 6.0 - } - self.titleCredibilityIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX - image.size.width, y: originY), size: image.size) - } + + self.titleCredibilityIconView.frame = CGRect(origin: CGPoint(x: titleFrame.maxX - titleCredibilitySize.width, y: floor((titleFrame.height - titleCredibilitySize.height) / 2.0)), size: titleCredibilitySize) + if let image = self.titleRightIconNode.image { self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX - image.size.width, y: titleFrame.minY + 6.0), size: image.size) } @@ -744,8 +776,6 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.pointerInteraction = PointerInteraction(view: self, style: .rectangle(CGSize(width: titleFrame.width + 16.0, height: 40.0))) } - - @objc private func buttonPressed() { self.pressed?() } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 53effa48c6..19e543fab9 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -22,6 +22,11 @@ import PeerInfoAvatarListNode import AnimationUI import ContextUI import ManagedAnimationNode +import ComponentFlow +import EmojiStatusComponent +import AnimationCache +import MultiAnimationRenderer +import ComponentDisplayAdapters enum PeerInfoHeaderButtonKey: Hashable { case message @@ -1985,8 +1990,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { let titleNodeContainer: ASDisplayNode let titleNodeRawContainer: ASDisplayNode let titleNode: MultiScaleTextNode - let titleCredibilityIconNode: ASImageNode - let titleExpandedCredibilityIconNode: ASImageNode + let titleCredibilityIconView: ComponentHostView + var credibilityIconSize: CGSize? + let titleExpandedCredibilityIconView: ComponentHostView + var titleExpandedCredibilityIconSize: CGSize? let subtitleNodeContainer: ASDisplayNode let subtitleNodeRawContainer: ASDisplayNode let subtitleNode: MultiScaleTextNode @@ -2023,6 +2030,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { var backgroundAlpha: CGFloat = 1.0 var updateHeaderAlpha: ((CGFloat, ContainedViewLayoutTransition) -> Void)? + let animationCache: AnimationCache + let animationRenderer: MultiAnimationRenderer + init(context: AccountContext, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, isMediaOnly: Bool, isSettings: Bool) { self.context = context self.isAvatarExpanded = avatarInitiallyExpanded @@ -2037,15 +2047,11 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.titleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded]) self.titleNode.displaysAsynchronously = false - self.titleCredibilityIconNode = ASImageNode() - self.titleCredibilityIconNode.displaysAsynchronously = false - self.titleCredibilityIconNode.displayWithoutProcessing = true - self.titleNode.stateNode(forKey: TitleNodeStateRegular)?.addSubnode(self.titleCredibilityIconNode) + self.titleCredibilityIconView = ComponentHostView() + self.titleNode.stateNode(forKey: TitleNodeStateRegular)?.view.addSubview(self.titleCredibilityIconView) - self.titleExpandedCredibilityIconNode = ASImageNode() - self.titleExpandedCredibilityIconNode.displaysAsynchronously = false - self.titleExpandedCredibilityIconNode.displayWithoutProcessing = true - self.titleNode.stateNode(forKey: TitleNodeStateExpanded)?.addSubnode(self.titleExpandedCredibilityIconNode) + self.titleExpandedCredibilityIconView = ComponentHostView() + self.titleNode.stateNode(forKey: TitleNodeStateExpanded)?.view.addSubview(self.titleExpandedCredibilityIconView) self.subtitleNodeContainer = ASDisplayNode() self.subtitleNodeRawContainer = ASDisplayNode() @@ -2099,6 +2105,11 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true + self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + self.animationRenderer = MultiAnimationRendererImpl() + super.init() requestUpdateLayoutImpl = { [weak self] in @@ -2183,11 +2194,11 @@ final class PeerInfoHeaderNode: ASDisplayNode { let phoneGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePhoneLongPress(_:))) self.subtitleNodeRawContainer.view.addGestureRecognizer(phoneGestureRecognizer) - let premiumGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleStarTap(_:))) + /*let premiumGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleStarTap(_:))) self.titleCredibilityIconNode.view.addGestureRecognizer(premiumGestureRecognizer) let expandedPremiumGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleStarTap(_:))) - self.titleExpandedCredibilityIconNode.view.addGestureRecognizer(expandedPremiumGestureRecognizer) + self.titleExpandedCredibilityIconNode.view.addGestureRecognizer(expandedPremiumGestureRecognizer)*/ } @objc private func handleUsernameLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { @@ -2202,12 +2213,12 @@ final class PeerInfoHeaderNode: ASDisplayNode { } } - @objc private func handleStarTap(_ gestureRecognizer: UITapGestureRecognizer) { + /*@objc private func handleStarTap(_ gestureRecognizer: UITapGestureRecognizer) { guard let view = gestureRecognizer.view, self.currentCredibilityIcon == .premium else { return } - self.displayPremiumIntro?(view, view == self.titleExpandedCredibilityIconNode.view) - } + self.displayPremiumIntro?(view, view == self.titleExpandedCredibilityIconView.componentView) + }*/ func initiateAvatarExpansion(gallery: Bool, first: Bool) { if let peer = self.peer, peer.profileImageRepresentations.isEmpty && gallery { @@ -2265,12 +2276,13 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.avatarListNode.listContainerNode.updateEntryIsHidden(entry: entry) } - private enum CredibilityIcon { + private enum CredibilityIcon: Equatable { case none case premium case verified case fake case scam + case emojiStatus(PeerEmojiStatus) } private var currentCredibilityIcon: CredibilityIcon? @@ -2303,7 +2315,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) let credibilityIcon: CredibilityIcon - if let peer = peer { + if let cachedData = cachedData as? CachedUserData, let emojiStatus = cachedData.emojiStatus { + credibilityIcon = .emojiStatus(emojiStatus) + } else if let peer = peer { if peer.isFake { credibilityIcon = .fake } else if peer.isScam { @@ -2321,6 +2335,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { if themeUpdated || self.currentCredibilityIcon != credibilityIcon { self.currentCredibilityIcon = credibilityIcon + let image: UIImage? var expandedImage: UIImage? @@ -2388,8 +2403,92 @@ final class PeerInfoHeaderNode: ASDisplayNode { image = nil } - self.titleCredibilityIconNode.image = image - self.titleExpandedCredibilityIconNode.image = expandedImage ?? image + let _ = image + let _ = expandedImage + + let emojiRegularStatusContent: EmojiStatusComponent.Content + let emojiExpandedStatusContent: EmojiStatusComponent.Content + switch credibilityIcon { + case .none: + emojiRegularStatusContent = .none + emojiExpandedStatusContent = .none + case .premium: + emojiRegularStatusContent = .premium(color: presentationData.theme.list.itemAccentColor) + emojiExpandedStatusContent = .premium(color: UIColor(rgb: 0xffffff, alpha: 0.75)) + case .verified: + emojiRegularStatusContent = .verified(fillColor: presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: presentationData.theme.list.itemCheckColors.foregroundColor) + emojiExpandedStatusContent = .verified(fillColor: UIColor(rgb: 0xffffff, alpha: 0.75), foregroundColor: .clear) + case .fake: + emojiRegularStatusContent = .fake(color: presentationData.theme.chat.message.incoming.scamColor) + emojiExpandedStatusContent = .fake(color: presentationData.theme.chat.message.incoming.scamColor) + case .scam: + emojiRegularStatusContent = .scam(color: presentationData.theme.chat.message.incoming.scamColor) + emojiExpandedStatusContent = .scam(color: presentationData.theme.chat.message.incoming.scamColor) + case let .emojiStatus(emojiStatus): + emojiRegularStatusContent = .emojiStatus(status: emojiStatus, placeholderColor: presentationData.theme.list.mediaPlaceholderColor) + emojiExpandedStatusContent = .emojiStatus(status: emojiStatus, placeholderColor: UIColor(rgb: 0xffffff, alpha: 0.15)) + } + + let iconSize = self.titleCredibilityIconView.update( + transition: Transition(transition), + component: AnyComponent(EmojiStatusComponent( + context: self.context, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, + content: emojiRegularStatusContent, + action: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.displayPremiumIntro?(strongSelf.titleCredibilityIconView, false) + }, + longTapAction: { [weak self] in + guard let strongSelf = self else { + return + } + let _ = strongSelf.context.engine.accountData.setEmojiStatus(file: nil).start() + } + )), + environment: {}, + containerSize: CGSize(width: 32.0, height: 32.0) + ) + let expandedIconSize = self.titleExpandedCredibilityIconView.update( + transition: Transition(transition), + component: AnyComponent(EmojiStatusComponent( + context: self.context, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, + content: emojiExpandedStatusContent, + action: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.displayPremiumIntro?(strongSelf.titleExpandedCredibilityIconView, false) + }, + longTapAction: { [weak self] in + guard let strongSelf = self else { + return + } + let _ = strongSelf.context.engine.accountData.setEmojiStatus(file: nil).start() + } + )), + environment: {}, + containerSize: CGSize(width: 32.0, height: 32.0) + ) + + self.credibilityIconSize = iconSize + self.titleExpandedCredibilityIconSize = expandedIconSize + + /*if let image = image { + self.credibilityIconSize = image.size + self.titleExpandedCredibilityIconSize = (expandedImage ?? image).size + } else { + self.credibilityIconSize = nil + self.titleExpandedCredibilityIconSize = nil + }*/ + + //self.titleCredibilityIconNode.image = image + //self.titleExpandedCredibilityIconNode.image = expandedImage ?? image } self.regularContentNode.alpha = state.isEditing ? 0.0 : 1.0 @@ -2610,10 +2709,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { let usernameSize = usernameNodeLayout[TitleNodeStateRegular]!.size var titleHorizontalOffset: CGFloat = 0.0 - if let image = self.titleCredibilityIconNode.image { - titleHorizontalOffset = -(image.size.width + 4.0) / 2.0 - transition.updateFrame(node: self.titleCredibilityIconNode, frame: CGRect(origin: CGPoint(x: titleSize.width + 4.0, y: floor((titleSize.height - image.size.height) / 2.0) + 1.0), size: image.size)) - transition.updateFrame(node: self.titleExpandedCredibilityIconNode, frame: CGRect(origin: CGPoint(x: titleExpandedSize.width + 4.0, y: floor((titleExpandedSize.height - image.size.height) / 2.0) + 1.0), size: image.size)) + if let credibilityIconSize = self.credibilityIconSize, let titleExpandedCredibilityIconSize = self.titleExpandedCredibilityIconSize { + titleHorizontalOffset = -(credibilityIconSize.width + 4.0) / 2.0 + transition.updateFrame(view: self.titleCredibilityIconView, frame: CGRect(origin: CGPoint(x: titleSize.width + 4.0, y: floor((titleSize.height - credibilityIconSize.height) / 2.0) + 1.0), size: credibilityIconSize)) + transition.updateFrame(view: self.titleExpandedCredibilityIconView, frame: CGRect(origin: CGPoint(x: titleExpandedSize.width + 4.0, y: floor((titleExpandedSize.height - titleExpandedCredibilityIconSize.height) / 2.0) + 1.0), size: titleExpandedCredibilityIconSize)) } var titleFrame: CGRect @@ -3098,13 +3197,18 @@ final class PeerInfoHeaderNode: ASDisplayNode { return nil } - if self.currentCredibilityIcon == .premium && !(self.state?.isEditing ?? false) { - let iconFrame = self.titleCredibilityIconNode.view.convert(self.titleCredibilityIconNode.bounds, to: self.view) - let expandedIconFrame = self.titleExpandedCredibilityIconNode.view.convert(self.titleExpandedCredibilityIconNode.bounds, to: self.view) - if expandedIconFrame.contains(point) && self.isAvatarExpanded { - return self.titleExpandedCredibilityIconNode.view - } else if iconFrame.contains(point) { - return self.titleCredibilityIconNode.view + if !(self.state?.isEditing ?? false) { + switch self.currentCredibilityIcon { + case .premium, .emojiStatus: + let iconFrame = self.titleCredibilityIconView.convert(self.titleCredibilityIconView.bounds, to: self.view) + let expandedIconFrame = self.titleExpandedCredibilityIconView.convert(self.titleExpandedCredibilityIconView.bounds, to: self.view) + if expandedIconFrame.contains(point) && self.isAvatarExpanded { + return self.titleExpandedCredibilityIconView.hitTest(self.view.convert(point, to: self.titleExpandedCredibilityIconView), with: event) + } else if iconFrame.contains(point) { + return self.titleCredibilityIconView.hitTest(self.view.convert(point, to: self.titleCredibilityIconView), with: event) + } + default: + break } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 38aab65897..9d0f6d9fb0 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -69,6 +69,9 @@ import CreateExternalMediaStreamScreen import PaymentMethodUI import PremiumUI import InstantPageCache +import EmojiStatusSelectionComponent +import AnimationCache +import MultiAnimationRenderer protocol PeerInfoScreenItem: AnyObject { var id: AnyHashable { get } @@ -3025,6 +3028,30 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } })) } + + self.headerNode.displayPremiumIntro = { [weak self] sourceView, white in + guard let strongSelf = self else { + return + } + + let animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + let animationRenderer = MultiAnimationRendererImpl() + strongSelf.controller?.present(EmojiStatusSelectionController( + context: strongSelf.context, + sourceView: sourceView, + emojiContent: ChatEntityKeyboardInputNode.emojiInputData( + context: strongSelf.context, + animationCache: animationCache, + animationRenderer: animationRenderer, + isStandalone: false, + areUnicodeEmojiEnabled: false, + areCustomEmojiEnabled: true, + chatPeerId: strongSelf.context.account.peerId + ) + ), in: .window(.root)) + } } else { screenData = peerInfoScreenData(context: context, peerId: peerId, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, isSettings: self.isSettings, hintGroupInCommon: hintGroupInCommon, existingRequestsContext: requestsContext) diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m index 9241d2297b..14c1ef1824 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m @@ -190,7 +190,9 @@ UIView * _Nullable makePortalView() { view.matchesPosition = true; view.matchesTransform = true; view.matchesAlpha = false; - view.allowsHitTesting = false; + if (@available(iOS 13.0, *)) { + view.allowsHitTesting = false; + } return view; } else {