diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift index 6178722b8c..2fa0012fc9 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift @@ -489,7 +489,7 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke let duration: Double = frameSource.frameRate > 0 ? Double(frameSource.frameCount) / Double(frameSource.frameRate) : 0 let frameRate = frameSource.frameRate - let timer = SwiftSignalKit.Timer(timeout: 1.0 / Double(frameRate), repeat: !firstFrame, completion: { + let timerEvent: () -> Void = { let frame = frameQueue.syncWith { frameQueue in return frameQueue.take(draw: true) } @@ -544,8 +544,13 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke frameQueue.with { frameQueue in frameQueue.generateFramesIfNeeded() } + } + + let timer = SwiftSignalKit.Timer(timeout: 1.0 / Double(frameRate), repeat: !firstFrame, completion: { + timerEvent() }, queue: queue) let _ = timerHolder.swap(timer) + timerEvent() timer.start() } } else { diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 14676d90c5..6cda64f143 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -172,6 +172,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private var didSetupTabs = false + private weak var emojiStatusSelectionController: ViewController? + public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) { if self.isNodeLoaded { self.chatListDisplayNode.containerNode.updateSelectedChatLocation(data: data as? ChatLocation, progress: progress, transition: transition) @@ -847,7 +849,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } private func openStatusSetup(sourceView: UIView) { - self.present(EmojiStatusSelectionController( + self.emojiStatusSelectionController?.dismiss() + let controller = EmojiStatusSelectionController( context: self.context, mode: .statusSelection, sourceView: sourceView, @@ -866,7 +869,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController destinationItemView: { [weak sourceView] in return sourceView } - ), in: .window(.root)) + ) + self.emojiStatusSelectionController = controller + self.present(controller, in: .window(.root)) } private func updateThemeAndStrings() { @@ -1899,6 +1904,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } return true }) + + if let emojiStatusSelectionController = self.emojiStatusSelectionController { + self.emojiStatusSelectionController = nil + emojiStatusSelectionController.dismiss() + } } override public func viewWillDisappear(_ animated: Bool) { diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index e4355f687f..f46135702b 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -459,7 +459,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let onlineNode: PeerOnlineMarkerNode let pinnedIconNode: ASImageNode var secretIconNode: ASImageNode? - var credibilityIconNode: ASImageNode? var credibilityIconView: ComponentHostView? let mutedIconNode: ASImageNode @@ -1071,7 +1070,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var currentMentionBadgeImage: UIImage? var currentPinnedIconImage: UIImage? var currentMutedIconImage: UIImage? - var currentCredibilityIconImage: UIImage? var currentCredibilityIconContent: EmojiStatusComponent.Content? var currentSecretIconImage: UIImage? @@ -1523,19 +1521,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { case let .peer(messages, _, _, _, _, _, _, _, _, _, _, _, _): if let peer = messages.last?.author { if case let .user(user) = peer, let emojiStatus = user.emojiStatus { - currentCredibilityIconImage = PresentationResourcesChatList.premiumIcon(item.presentationData.theme) currentCredibilityIconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor) } else 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) currentCredibilityIconContent = .premium(color: item.presentationData.theme.list.itemAccentColor) } } @@ -1544,19 +1537,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer { if case let .user(user) = peer, let emojiStatus = user.emojiStatus { - currentCredibilityIconImage = PresentationResourcesChatList.premiumIcon(item.presentationData.theme) currentCredibilityIconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor) } else 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) currentCredibilityIconContent = .premium(color: item.presentationData.theme.list.itemAccentColor) } } @@ -1564,13 +1552,18 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if let currentSecretIconImage = currentSecretIconImage { titleIconsWidth += currentSecretIconImage.size.width + 2.0 } - if let currentCredibilityIconImage = currentCredibilityIconImage { + if let currentCredibilityIconContent = currentCredibilityIconContent { if titleIconsWidth.isZero { titleIconsWidth += 4.0 } else { titleIconsWidth += 2.0 } - titleIconsWidth += currentCredibilityIconImage.size.width + switch currentCredibilityIconContent { + case .fake, .scam: + titleIconsWidth += 14.0 + default: + titleIconsWidth += 8.0 + } } let layoutOffset: CGFloat = 0.0 @@ -2077,32 +2070,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { 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)) + nextTitleIconOrigin += credibilityIconView.bounds.width + 4.0 } else if let credibilityIconView = strongSelf.credibilityIconView { strongSelf.credibilityIconView = nil credibilityIconView.removeFromSuperview() } - if let currentCredibilityIconImage = currentCredibilityIconImage { - let iconNode: ASImageNode - if let current = strongSelf.credibilityIconNode { - iconNode = current - } else { - iconNode = ASImageNode() - iconNode.isLayerBacked = true - iconNode.displaysAsynchronously = false - 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)) - nextTitleIconOrigin += currentCredibilityIconImage.size.width + 4.0 - } else if let credibilityIconNode = strongSelf.credibilityIconNode { - strongSelf.credibilityIconNode = nil - credibilityIconNode.removeFromSupernode() - } - if let currentMutedIconImage = currentMutedIconImage { strongSelf.mutedIconNode.image = currentMutedIconImage strongSelf.mutedIconNode.isHidden = false @@ -2359,10 +2332,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if let credibilityIconView = self.credibilityIconView { transition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: credibilityIconView.frame.origin.y), size: credibilityIconView.bounds.size)) - nextTitleIconOrigin += credibilityIconView.bounds.size.width + 5.0 - } else if let credibilityIconNode = self.credibilityIconNode { - transition.updateFrame(node: credibilityIconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: credibilityIconNode.frame.origin.y), size: credibilityIconNode.bounds.size)) - nextTitleIconOrigin += credibilityIconNode.bounds.size.width + 5.0 + nextTitleIconOrigin += credibilityIconView.bounds.size.width + 4.0 } let mutedIconFrame = self.mutedIconNode.frame diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index a6b076a22d..e71b760fa3 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -97,7 +97,7 @@ public final class ReactionIconView: PortalSourceView { case .builtin: iconSize = CGSize(width: floor(size.width * 2.0), height: floor(size.height * 2.0)) case .custom: - iconSize = size + iconSize = CGSize(width: floor(size.width * 1.25), height: floor(size.height * 1.25)) } transition.updateFrame(layer: animationLayer, frame: CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)) @@ -125,9 +125,9 @@ public final class ReactionIconView: PortalSourceView { let iconSize: CGSize switch reaction { case .builtin: - iconSize = CGSize(width: floor(size.width * 1.5), height: floor(size.height * 1.5)) + iconSize = CGSize(width: floor(size.width * 2.0), height: floor(size.height * 2.0)) case .custom: - iconSize = size + iconSize = CGSize(width: floor(size.width * 1.25), height: floor(size.height * 1.25)) } let animationLayer = InlineStickerItemLayer( @@ -653,8 +653,13 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { public var activateAfterCompletion: Bool = false { didSet { if self.activateAfterCompletion { - self.contextGesture?.activatedAfterCompletion = { [weak self] in - self?.pressed() + self.contextGesture?.activatedAfterCompletion = { [weak self] point in + guard let strongSelf = self else { + return + } + if strongSelf.buttonNode.bounds.contains(point) { + strongSelf.pressed() + } } } else { self.contextGesture?.activatedAfterCompletion = nil @@ -692,8 +697,13 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { } if self.activateAfterCompletion { - self.contextGesture?.activatedAfterCompletion = { [weak self] in - self?.pressed() + self.contextGesture?.activatedAfterCompletion = { [weak self] point in + guard let strongSelf = self else { + return + } + if strongSelf.buttonNode.bounds.contains(point) { + strongSelf.pressed() + } } } } diff --git a/submodules/ContextUI/BUILD b/submodules/ContextUI/BUILD index 037f119642..f0860edb01 100644 --- a/submodules/ContextUI/BUILD +++ b/submodules/ContextUI/BUILD @@ -23,6 +23,7 @@ swift_library( "//submodules/TextFormat:TextFormat", "//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities", "//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard", + "//submodules/UndoUI:UndoUI", ], visibility = [ "//visibility:public", diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index efcacf3237..96b2a2be29 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -12,6 +12,7 @@ import TextNodeWithEntities import EntityKeyboard import AnimationCache import MultiAnimationRenderer +import UndoUI private let animationDurationFactor: Double = 1.0 @@ -1531,7 +1532,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } controller.reactionSelected?(reaction, isLarge) } - reactionContextNode.premiumReactionsSelected = { [weak self] in + reactionContextNode.premiumReactionsSelected = { [weak self] _ in guard let strongSelf = self, let controller = strongSelf.getController() as? ContextController else { return } @@ -2140,6 +2141,22 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi return nil } + if let controller = self.getController() as? ContextController { + var innerResult: UIView? + controller.forEachController { c in + if let c = c as? UndoOverlayController { + if let result = c.view.hitTest(self.view.convert(point, to: c.view), with: event) { + innerResult = result + return false + } + } + return true + } + if let innerResult = innerResult { + return innerResult + } + } + if let presentationNode = self.presentationNode { return presentationNode.hitTest(self.view.convert(point, to: presentationNode.view), with: event) } @@ -2583,7 +2600,18 @@ public final class ContextController: ViewController, StandalonePresentableContr self.displayNode = ContextControllerNode(account: self.account, controller: self, presentationData: self.presentationData, source: self.source, items: self.items, beginDismiss: { [weak self] result in self?.dismiss(result: result, completion: nil) }, recognizer: self.recognizer, gesture: self.gesture, beganAnimatingOut: { [weak self] in - self?.statusBar.statusBarStyle = .Ignore + guard let strongSelf = self else { + return + } + + strongSelf.statusBar.statusBarStyle = .Ignore + + strongSelf.forEachController { c in + if let c = c as? UndoOverlayController { + c.dismiss() + } + return true + } }, attemptTransitionControllerIntoNavigation: { [weak self] in guard let strongSelf = self else { return diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 1a213193dc..a7ebbdaf55 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -7,6 +7,7 @@ import TextSelectionNode import TelegramCore import SwiftSignalKit import ReactionSelectionNode +import UndoUI private extension ContextControllerTakeViewInfo.ContainingItem { var contentRect: CGRect { @@ -189,6 +190,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo private let contentRectDebugNode: ASDisplayNode private let actionsStackNode: ContextControllerActionsStackNode + private var validLayout: ContainerViewLayout? private var animatingOutState: AnimatingOutState? private var strings: PresentationStrings? @@ -201,6 +203,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo private var overscrollMode: OverscrollMode = .unrestricted + private weak var currentUndoController: ViewController? + init( getController: @escaping () -> ContextControllerProtocol?, requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, @@ -437,6 +441,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo transition: ContainedViewLayoutTransition, stateTransition: ContextControllerPresentationNodeStateTransition? ) { + self.validLayout = layout + let contentActionsSpacing: CGFloat = 7.0 let actionsEdgeInset: CGFloat let actionsSideInset: CGFloat = 6.0 @@ -540,11 +546,37 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } controller.reactionSelected?(reaction, isLarge) } - reactionContextNode.premiumReactionsSelected = { [weak self] in - guard let strongSelf = self, let controller = strongSelf.getController() as? ContextController else { + let context = reactionItems.context + reactionContextNode.premiumReactionsSelected = { [weak self] file in + guard let strongSelf = self, let validLayout = strongSelf.validLayout, let controller = strongSelf.getController() as? ContextController else { return } - controller.premiumReactionsSelected?() + + if let file = file, let reactionContextNode = strongSelf.reactionContextNode { + let position: UndoOverlayController.Position + let insets = validLayout.insets(options: .statusBar) + if reactionContextNode.hasSpaceInTheBottom(insets: insets, height: 100.0) { + position = .bottom + } else { + position = .top + } + + var animateInAsReplacement = false + if let currentUndoController = strongSelf.currentUndoController { + currentUndoController.dismiss() + animateInAsReplacement = true + } + + //TODO:localize + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: "Subscribe to **Telegram Premium** to unlock this reaction.", undoText: "More", customAction: { [weak controller] in + controller?.premiumReactionsSelected?() + }), elevatedLayout: false, position: position, animateInAsReplacement: animateInAsReplacement, action: { _ in true }) + strongSelf.currentUndoController = undoController + controller.present(undoController, in: .current) + } else { + controller.premiumReactionsSelected?() + } } } contentTopInset += reactionContextNode.contentHeight + 18.0 diff --git a/submodules/Display/Source/ContextGesture.swift b/submodules/Display/Source/ContextGesture.swift index 217df908aa..a3aa7b917a 100644 --- a/submodules/Display/Source/ContextGesture.swift +++ b/submodules/Display/Source/ContextGesture.swift @@ -68,7 +68,7 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg public var activated: ((ContextGesture, CGPoint) -> Void)? public var externalUpdated: ((UIView?, CGPoint) -> Void)? public var externalEnded: (((UIView?, CGPoint)?) -> Void)? - public var activatedAfterCompletion: (() -> Void)? + public var activatedAfterCompletion: ((CGPoint) -> Void)? public var cancelGesturesOnActivation: (() -> Void)? override public init(target: Any?, action: Selector?) { @@ -208,7 +208,7 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg self.currentProgress = 0.0 self.activationProgress?(0.0, .ended(self.currentProgress)) if self.wasActivated { - self.activatedAfterCompletion?() + self.activatedAfterCompletion?(touch.location(in: self.view)) } } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 0934c60888..fe910f207b 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -160,7 +160,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private var customReactionSource: (view: UIView, rect: CGRect, layer: CALayer, item: ReactionItem)? public var reactionSelected: ((UpdateMessageReaction, Bool) -> Void)? - public var premiumReactionsSelected: (() -> Void)? + public var premiumReactionsSelected: ((TelegramMediaFile?) -> Void)? private var hapticFeedback: HapticFeedback? private var standaloneReactionAnimation: StandaloneReactionAnimation? @@ -196,6 +196,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private var availableReactions: AvailableReactions? private var availableReactionsDisposable: Disposable? + private var hasPremium: Bool? + private var hasPremiumDisposable: Disposable? + public init(context: AccountContext, animationCache: AnimationCache, presentationData: PresentationData, items: [ReactionContextItem], getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?, isExpandedUpdated: @escaping (ContainedViewLayoutTransition) -> Void, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void) { self.context = context self.presentationData = presentationData @@ -277,8 +280,6 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.contentContainer.view.addSubview(expandItemView) self.contentTintContainer.view.addSubview(expandItemView.tintView) - - self.canBeExpanded = true } else { self.expandItemView = nil } @@ -292,12 +293,6 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.addSubnode(self.contentContainer) self.addSubnode(self.previewingItemContainer) - if self.canBeExpanded { - let horizontalExpandRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.horizontalExpandGesture(_:))) - self.view.addGestureRecognizer(horizontalExpandRecognizer) - self.horizontalExpandRecognizer = horizontalExpandRecognizer - } - self.availableReactionsDisposable = (context.engine.stickers.availableReactions() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] availableReactions in @@ -306,11 +301,62 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } strongSelf.availableReactions = availableReactions }) + + self.hasPremiumDisposable = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let strongSelf = self else { + return + } + strongSelf.hasPremium = peer?.isPremium ?? false + }) + + if let getEmojiContent = getEmojiContent { + self.emojiContentDisposable = (getEmojiContent(self.animationCache, self.animationRenderer) + |> deliverOnMainQueue).start(next: { [weak self] emojiContent in + guard let strongSelf = self else { + return + } + + strongSelf.emojiContent = emojiContent + if !strongSelf.canBeExpanded { + strongSelf.canBeExpanded = true + + let horizontalExpandRecognizer = UIPanGestureRecognizer(target: strongSelf, action: #selector(strongSelf.horizontalExpandGesture(_:))) + strongSelf.view.addGestureRecognizer(horizontalExpandRecognizer) + strongSelf.horizontalExpandRecognizer = horizontalExpandRecognizer + } + strongSelf.updateEmojiContent(emojiContent) + + if let reactionSelectionComponentHost = strongSelf.reactionSelectionComponentHost, let componentView = reactionSelectionComponentHost.view { + var emojiTransition: Transition = .immediate + if let scheduledEmojiContentAnimationHint = strongSelf.scheduledEmojiContentAnimationHint { + strongSelf.scheduledEmojiContentAnimationHint = nil + let contentAnimation = scheduledEmojiContentAnimationHint + emojiTransition = Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) + } + + let _ = reactionSelectionComponentHost.update( + transition: emojiTransition, + component: AnyComponent(EmojiStatusSelectionComponent( + theme: strongSelf.presentationData.theme, + strings: strongSelf.presentationData.strings, + deviceMetrics: DeviceMetrics.iPhone13, + emojiContent: emojiContent, + backgroundColor: .clear, + separatorColor: strongSelf.presentationData.theme.list.itemPlainSeparatorColor.withMultipliedAlpha(0.5) + )), + environment: {}, + containerSize: CGSize(width: componentView.bounds.width, height: 300.0) + ) + } + }) + } } deinit { self.emojiContentDisposable?.dispose() self.availableReactionsDisposable?.dispose() + self.hasPremiumDisposable?.dispose() } override public func didLoad() { @@ -339,13 +385,18 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { var compressionFactor: CGFloat = max(0.0, min(1.0, self.horizontalExpandDistance / maxCompressionDistance)) compressionFactor = compressionFactor * compressionFactor - self.extensionDistance = 20.0 * compressionFactor - self.visibleExtensionDistance = self.extensionDistance - - self.requestLayout(.immediate) + if compressionFactor >= 0.95 { + self.horizontalExpandStartLocation = nil + self.expand() + } else { + self.extensionDistance = 20.0 * compressionFactor + self.visibleExtensionDistance = self.extensionDistance + + self.requestLayout(.immediate) + } } case .cancelled, .ended: - if self.horizontalExpandDistance != 0.0 { + if let _ = self.horizontalExpandStartLocation, self.horizontalExpandDistance != 0.0 { if self.horizontalExpandDistance >= 90.0 { self.expand() } else { @@ -620,6 +671,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if self.getEmojiContent != nil && i == visibleItemCount - 1 { transition.updateSublayerTransformScale(node: itemNode, scale: 0.001 * (1.0 - compressionFactor) + 1.0 * compressionFactor) + + let alphaFraction = min(compressionFactor, 0.2) / 0.2 + transition.updateAlpha(node: itemNode, alpha: alphaFraction) } } } @@ -745,7 +799,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { itemSpacing: itemSpacing ) - if (self.isExpanded || self.reactionSelectionComponentHost != nil), let getEmojiContent = self.getEmojiContent { + if (self.isExpanded || self.reactionSelectionComponentHost != nil), let _ = self.getEmojiContent { let reactionSelectionComponentHost: ComponentView var componentTransition = Transition(transition) if let current = self.reactionSelectionComponentHost { @@ -756,57 +810,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.reactionSelectionComponentHost = reactionSelectionComponentHost } - var emojiContent: EmojiPagerContentComponent? - if let current = self.emojiContent { - emojiContent = current - } else { - let semaphore = DispatchSemaphore(value: 0) - let _ = (getEmojiContent(self.animationCache, self.animationRenderer) |> take(1)).start(next: { value in - emojiContent = value - semaphore.signal() - }) - - semaphore.wait() - self.emojiContent = emojiContent - if let emojiContent = emojiContent { - self.updateEmojiContent(emojiContent) - } - - self.emojiContentDisposable = (getEmojiContent(self.animationCache, self.animationRenderer) - |> deliverOnMainQueue).start(next: { [weak self] emojiContent in - guard let strongSelf = self else { - return - } - - strongSelf.emojiContent = emojiContent - strongSelf.updateEmojiContent(emojiContent) - - if let reactionSelectionComponentHost = strongSelf.reactionSelectionComponentHost, let componentView = reactionSelectionComponentHost.view { - var emojiTransition: Transition = .immediate - if let scheduledEmojiContentAnimationHint = strongSelf.scheduledEmojiContentAnimationHint { - strongSelf.scheduledEmojiContentAnimationHint = nil - let contentAnimation = scheduledEmojiContentAnimationHint - emojiTransition = Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) - } - - let _ = reactionSelectionComponentHost.update( - transition: emojiTransition, - component: AnyComponent(EmojiStatusSelectionComponent( - theme: strongSelf.presentationData.theme, - strings: strongSelf.presentationData.strings, - deviceMetrics: DeviceMetrics.iPhone13, - emojiContent: emojiContent, - backgroundColor: .clear, - separatorColor: strongSelf.presentationData.theme.list.itemPlainSeparatorColor.withMultipliedAlpha(0.5) - )), - environment: {}, - containerSize: CGSize(width: componentView.bounds.width, height: 300.0) - ) - } - }) - } - - if let emojiContent = emojiContent { + if let emojiContent = self.emojiContent { self.updateEmojiContent(emojiContent) if let scheduledEmojiContentAnimationHint = self.scheduledEmojiContentAnimationHint { @@ -848,6 +852,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { guard let placeholder = itemNode.currentFrameImage else { continue } + if itemNode.alpha.isZero { + continue + } initialPositionAndFrame[itemNode.item.stillAnimation.fileId] = ( position: itemNode.frame.center, frameIndex: itemNode.currentFrameIndex, @@ -974,8 +981,12 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { isCustom: false ) - strongSelf.customReactionSource = (sourceView, sourceRect, sourceLayer, reactionItem) - strongSelf.reactionSelected?(updateReaction, isLongPress) + if case .custom = reactionItem.updateMessageReaction, let hasPremium = strongSelf.hasPremium, !hasPremium { + strongSelf.premiumReactionsSelected?(reactionItem.stillAnimation) + } else { + strongSelf.customReactionSource = (sourceView, sourceRect, sourceLayer, reactionItem) + strongSelf.reactionSelected?(updateReaction, isLongPress) + } break } @@ -992,7 +1003,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { isCustom: true ) strongSelf.customReactionSource = (sourceView, sourceRect, sourceLayer, reactionItem) - strongSelf.reactionSelected?(reactionItem.updateMessageReaction, isLongPress) + if case .custom = reactionItem.updateMessageReaction, let hasPremium = strongSelf.hasPremium, !hasPremium { + strongSelf.premiumReactionsSelected?(reactionItem.stillAnimation) + } else { + strongSelf.reactionSelected?(reactionItem.updateMessageReaction, isLongPress) + } } }, deleteBackwards: { @@ -1007,7 +1022,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } if isPremiumLocked { - strongSelf.premiumReactionsSelected?() + strongSelf.premiumReactionsSelected?(nil) return } @@ -1594,9 +1609,13 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } else if let reaction = self.reaction(at: point) { switch reaction { case let .reaction(reactionItem): - self.reactionSelected?(reactionItem.updateMessageReaction, false) + if case .custom = reactionItem.updateMessageReaction, let hasPremium = self.hasPremium, !hasPremium { + self.premiumReactionsSelected?(reactionItem.stillAnimation) + } else { + self.reactionSelected?(reactionItem.updateMessageReaction, false) + } case .premium: - self.premiumReactionsSelected?() + self.premiumReactionsSelected?(nil) } } default: @@ -1604,6 +1623,14 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } + public func hasSpaceInTheBottom(insets: UIEdgeInsets, height: CGFloat) -> Bool { + if self.backgroundNode.frame.maxY < self.bounds.height - insets.bottom - height { + return true + } else { + return false + } + } + public func expand() { if self.hapticFeedback == nil { self.hapticFeedback = HapticFeedback() @@ -1720,7 +1747,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { public func performReactionSelection(reaction: ReactionItem.Reaction, isLarge: Bool) { for (_, itemNode) in self.visibleItemNodes { if let itemNode = itemNode as? ReactionNode, itemNode.item.reaction == reaction { - self.reactionSelected?(itemNode.item.updateMessageReaction, isLarge) + if case .custom = itemNode.item.updateMessageReaction, let hasPremium = self.hasPremium, !hasPremium { + self.premiumReactionsSelected?(itemNode.item.stillAnimation) + } else { + self.reactionSelected?(itemNode.item.updateMessageReaction, isLarge) + } break } } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift index d56d9d2b35..b468ceff9a 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift @@ -361,7 +361,8 @@ final class StickerPackEmojisItemNode: GridItemNode { animationData: animationData, content: .animation(animationData), itemFile: item.file, - subgroupId: nil + subgroupId: nil, + icon: .none ), context: context, attemptSynchronousLoad: attemptSynchronousLoads, diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift index fe535608a8..bca04443eb 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift @@ -16,6 +16,8 @@ import AccountContext import LegacyComponents import AudioBlob import PeerInfoAvatarListNode +import ComponentFlow +import EmojiStatusComponent final class VoiceChatParticipantItem: ListViewItem { enum ParticipantText: Equatable { @@ -262,7 +264,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { private let titleNode: TextNode private let statusNode: VoiceChatParticipantStatusNode private let expandedStatusNode: VoiceChatParticipantStatusNode - private var credibilityIconNode: ASImageNode? + private var credibilityIconView: ComponentHostView? private var avatarTransitionNode: ASImageNode? private var avatarListContainerNode: ASDisplayNode? @@ -832,24 +834,28 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 }) var titleIconsWidth: CGFloat = 0.0 - var currentCredibilityIconImage: UIImage? - var credibilityIconOffset: CGFloat = 0.0 + + var credibilityIcon: EmojiStatusComponent.Content? if item.peer.isScam { - currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) - credibilityIconOffset = 2.0 + credibilityIcon = .scam(color: item.presentationData.theme.chat.message.incoming.scamColor) } else if item.peer.isFake { - currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) - credibilityIconOffset = 2.0 + credibilityIcon = .fake(color: item.presentationData.theme.chat.message.incoming.scamColor) + } else if let user = item.peer as? TelegramUser, let emojiStatus = user.emojiStatus { + credibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor) } else if item.peer.isVerified { - currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme) - credibilityIconOffset = 3.0 + credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) } else if item.peer.isPremium && !premiumConfiguration.isPremiumDisabled { - currentCredibilityIconImage = PresentationResourcesChatList.premiumIcon(item.presentationData.theme) - credibilityIconOffset = 3.0 + credibilityIcon = .premium(color: item.presentationData.theme.list.itemAccentColor) } - if let currentCredibilityIconImage = currentCredibilityIconImage { - titleIconsWidth += 4.0 + currentCredibilityIconImage.size.width + if let credibilityIcon = credibilityIcon { + titleIconsWidth += 4.0 + switch credibilityIcon { + case .scam, .fake: + titleIconsWidth += 30.0 + default: + titleIconsWidth += 16.0 + } } var expandedRightInset: CGFloat = 30.0 @@ -1008,23 +1014,38 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout)) transition.updateFrame(node: strongSelf.expandedStatusNode, frame: CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: expandedStatusLayout)) - if let currentCredibilityIconImage = currentCredibilityIconImage { - let iconNode: ASImageNode - if let current = strongSelf.credibilityIconNode { - iconNode = current + if let credibilityIcon = credibilityIcon { + let animationCache = item.context.animationCache + let animationRenderer = item.context.animationRenderer + + let credibilityIconView: ComponentHostView + if let current = strongSelf.credibilityIconView { + credibilityIconView = current } else { - iconNode = ASImageNode() - iconNode.isLayerBacked = true - iconNode.displaysAsynchronously = false - iconNode.displayWithoutProcessing = true - strongSelf.offsetContainerNode.addSubnode(iconNode) - strongSelf.credibilityIconNode = iconNode + credibilityIconView = ComponentHostView() + strongSelf.offsetContainerNode.view.addSubview(credibilityIconView) + strongSelf.credibilityIconView = credibilityIconView } - iconNode.image = currentCredibilityIconImage - transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: leftInset + titleLayout.size.width + 3.0, y: verticalInset + credibilityIconOffset), size: currentCredibilityIconImage.size)) - } else if let credibilityIconNode = strongSelf.credibilityIconNode { - strongSelf.credibilityIconNode = nil - credibilityIconNode.removeFromSupernode() + + let iconSize = credibilityIconView.update( + transition: .immediate, + component: AnyComponent(EmojiStatusComponent( + context: item.context, + animationCache: animationCache, + animationRenderer: animationRenderer, + content: credibilityIcon, + action: nil, + longTapAction: nil, + emojiFileUpdated: nil + )), + environment: {}, + containerSize: CGSize(width: 20.0, height: 20.0) + ) + + transition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: leftInset + titleLayout.size.width + 3.0, y: verticalInset + floor((titleFrame.height - iconSize.height) / 2.0)), size: iconSize)) + } else if let credibilityIconView = strongSelf.credibilityIconView { + strongSelf.credibilityIconView = nil + credibilityIconView.removeFromSuperview() } transition.updateFrameAsPositionAndBounds(node: strongSelf.avatarNode, frame: avatarFrame) diff --git a/submodules/TelegramCore/Sources/State/MessageReactions.swift b/submodules/TelegramCore/Sources/State/MessageReactions.swift index f24ff8d841..044e34cd9b 100644 --- a/submodules/TelegramCore/Sources/State/MessageReactions.swift +++ b/submodules/TelegramCore/Sources/State/MessageReactions.swift @@ -61,7 +61,7 @@ public func updateMessageReactionsInteractively(account: Account, messageId: Mes for attribute in currentMessage.attributes { if let attribute = attribute as? ReactionsMessageAttribute { for updatedReaction in reactions { - if !attribute.reactions.contains(where: { $0.value == updatedReaction.reaction }) { + if !attribute.reactions.contains(where: { $0.value == updatedReaction.reaction && $0.isSelected }) { let recentReactionItem: RecentReactionItem switch updatedReaction { case let .builtin(value): diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MessageReadStats.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MessageReadStats.swift index f7638f5c8c..8b5f743e85 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MessageReadStats.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MessageReadStats.swift @@ -43,26 +43,25 @@ func _internal_messageReadStats(account: Account, id: MessageId) -> Signal mapToSignal { result, reactionCount -> Signal in - guard let result = result else { - return .single(nil) - } return account.postbox.transaction { transaction -> (peerIds: [PeerId], missingPeerIds: [PeerId]) in var peerIds: [PeerId] = [] var missingPeerIds: [PeerId] = [] let authorId = transaction.getMessage(id)?.author?.id - for id in result { - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)) - if peerId == account.peerId { - continue - } - if peerId == authorId { - continue - } - peerIds.append(peerId) - if transaction.getPeer(peerId) == nil { - missingPeerIds.append(peerId) + if let result = result { + for id in result { + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)) + if peerId == account.peerId { + continue + } + if peerId == authorId { + continue + } + peerIds.append(peerId) + if transaction.getPeer(peerId) == nil { + missingPeerIds.append(peerId) + } } } diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD index 8a8c95fd9b..85425290f3 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD @@ -27,6 +27,7 @@ swift_library( "//submodules/lottie-ios:Lottie", "//submodules/TextFormat:TextFormat", "//submodules/AppBundle:AppBundle", + "//submodules/GZip:GZip", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index 867af08ae5..54ac66dfb1 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -17,6 +17,7 @@ import Lottie import EmojiTextAttachmentView import TextFormat import AppBundle +import GZip public final class EmojiStatusSelectionComponent: Component { public typealias EnvironmentType = Empty @@ -105,7 +106,7 @@ public final class EmojiStatusSelectionComponent: Component { let topPanelHeight: CGFloat = 42.0 let keyboardSize = self.keyboardView.update( - transition: transition, + transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)), component: AnyComponent(EntityKeyboardComponent( theme: component.theme, strings: component.strings, @@ -335,7 +336,7 @@ public final class EmojiStatusSelectionController: ViewController { self.cloudShadowLayer1.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) } - func animateOutToStatus(groupId: AnyHashable, item: EmojiPagerContentComponent.Item, destinationView: UIView) { + func animateOutToStatus(groupId: AnyHashable, item: EmojiPagerContentComponent.Item, customEffectFile: String?, destinationView: UIView) { guard let emojiView = self.componentHost.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View, let sourceLayer = emojiView.layerForItem( groupId: groupId, item: item) else { self.controller?.dismiss() return @@ -364,7 +365,15 @@ public final class EmojiStatusSelectionController: ViewController { } var effectView: AnimationView? - if let itemFile = item.itemFile, let url = getAppBundle().url(forResource: "generic_reaction_small_effect", withExtension: "json"), let composition = Animation.filepath(url.path) { + + if let customEffectFile = customEffectFile, let data = try? Data(contentsOf: URL(fileURLWithPath: customEffectFile)), let composition = try? Animation.from(data: TGGUnzipData(data, 2 * 1024 * 1024) ?? data) { + let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable)) + view.animationSpeed = 1.0 + view.backgroundColor = nil + view.isOpaque = false + + effectView = view + } else if let itemFile = item.itemFile, let url = getAppBundle().url(forResource: "generic_reaction_small_effect", withExtension: "json"), let composition = Animation.filepath(url.path) { let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable)) view.animationSpeed = 1.0 view.backgroundColor = nil @@ -661,7 +670,52 @@ public final class EmojiStatusSelectionController: ViewController { } if let item = item, let destinationView = controller.destinationItemView() { - self.animateOutToStatus(groupId: groupId, item: item, destinationView: destinationView) + var emojiString: String? + if let itemFile = item.itemFile { + attributeLoop: for attribute in itemFile.attributes { + switch attribute { + case let .CustomEmoji(_, alt, _): + emojiString = alt + break attributeLoop + default: + break + } + } + } + + let context = self.context + let _ = (context.engine.stickers.availableReactions() + |> take(1) + |> mapToSignal { availableReactions -> Signal in + guard let emojiString = emojiString, let availableReactions = availableReactions else { + return .single(nil) + } + for reaction in availableReactions.reactions { + if case let .builtin(value) = reaction.value, value == emojiString { + if let aroundAnimation = reaction.aroundAnimation { + return context.account.postbox.mediaBox.resourceData(aroundAnimation.resource) + |> take(1) + |> map { data -> String? in + if data.complete { + return data.path + } else { + return nil + } + } + } else { + return .single(nil) + } + } + } + return .single(nil) + } + |> deliverOnMainQueue).start(next: { [weak self] filePath in + guard let strongSelf = self else { + return + } + + strongSelf.animateOutToStatus(groupId: groupId, item: item, customEffectFile: filePath, destinationView: destinationView) + }) } else { controller.dismiss() } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 13bbd94880..040ce2b079 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -179,6 +179,16 @@ private final class WarpView: UIView { } } +public struct EmojiComponentReactionItem { + public var reaction: MessageReaction.Reaction + public var file: TelegramMediaFile + + public init(reaction: MessageReaction.Reaction, file: TelegramMediaFile) { + self.reaction = reaction + self.file = file + } +} + public final class EntityKeyboardAnimationData: Equatable { public enum Id: Hashable { case file(MediaId) @@ -1506,6 +1516,14 @@ public final class EmojiPagerContentComponent: Component { } } + public final class SynchronousLoadBehavior { + public let isDisabled: Bool + + public init(isDisabled: Bool) { + self.isDisabled = isDisabled + } + } + public struct CustomLayout: Equatable { public var itemsPerRow: Int public var itemSize: CGFloat @@ -1633,21 +1651,30 @@ public final class EmojiPagerContentComponent: Component { } public final class Item: Equatable { + public enum Icon: Equatable { + case none + case locked + case premium + } + public let animationData: EntityKeyboardAnimationData? public let content: ItemContent public let itemFile: TelegramMediaFile? public let subgroupId: Int32? + public let icon: Icon public init( animationData: EntityKeyboardAnimationData?, content: ItemContent, itemFile: TelegramMediaFile?, - subgroupId: Int32? + subgroupId: Int32?, + icon: Icon ) { self.animationData = animationData self.content = content self.itemFile = itemFile self.subgroupId = subgroupId + self.icon = icon } public static func ==(lhs: Item, rhs: Item) -> Bool { @@ -1666,6 +1693,9 @@ public final class EmojiPagerContentComponent: Component { if lhs.subgroupId != rhs.subgroupId { return false } + if lhs.icon != rhs.icon { + return false + } return true } @@ -3953,6 +3983,15 @@ public final class EmojiPagerContentComponent: Component { var badge: ItemLayer.Badge? if itemGroup.displayPremiumBadges, let file = item.itemFile, file.isPremiumSticker { badge = .premium + } else { + switch item.icon { + case .none: + break + case .locked: + badge = .locked + case .premium: + badge = .premium + } } itemLayer.update(transition: transition, size: itemFrame.size, badge: badge, blurredBadgeColor: UIColor(white: 0.0, alpha: 0.1), blurredBadgeBackgroundColor: keyboardChildEnvironment.theme.list.plainBackgroundColor) @@ -4649,7 +4688,14 @@ public final class EmojiPagerContentComponent: Component { previousAbsoluteItemPositions[id] = position.offsetBy(dx: 0.0, dy: animatedScrollOffset) } - self.updateVisibleItems(transition: itemTransition, attemptSynchronousLoads: !(scrollView.isDragging || scrollView.isDecelerating), previousItemPositions: previousItemPositions, previousAbsoluteItemPositions: previousAbsoluteItemPositions, updatedItemPositions: updatedItemPositions, hintDisappearingGroupFrame: hintDisappearingGroupFrame) + var attemptSynchronousLoads = !(scrollView.isDragging || scrollView.isDecelerating) + if let synchronousLoadBehavior = transition.userData(SynchronousLoadBehavior.self) { + if synchronousLoadBehavior.isDisabled { + attemptSynchronousLoads = false + } + } + + self.updateVisibleItems(transition: itemTransition, attemptSynchronousLoads: attemptSynchronousLoads, previousItemPositions: previousItemPositions, previousAbsoluteItemPositions: previousAbsoluteItemPositions, updatedItemPositions: updatedItemPositions, hintDisappearingGroupFrame: hintDisappearingGroupFrame) return availableSize } @@ -4680,7 +4726,7 @@ public final class EmojiPagerContentComponent: Component { return hasPremium } - public static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool, isStatusSelection: Bool, isReactionSelection: Bool, isQuickReactionSelection: Bool = false, topReactionItems: [AvailableReactions.Reaction], areUnicodeEmojiEnabled: Bool, areCustomEmojiEnabled: Bool, chatPeerId: EnginePeer.Id?, selectedItems: Set = Set()) -> Signal { + public static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool, isStatusSelection: Bool, isReactionSelection: Bool, isQuickReactionSelection: Bool = false, topReactionItems: [EmojiComponentReactionItem], areUnicodeEmojiEnabled: Bool, areCustomEmojiEnabled: Bool, chatPeerId: EnginePeer.Id?, selectedItems: Set = Set()) -> Signal { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let isPremiumDisabled = premiumConfiguration.isPremiumDisabled @@ -4751,7 +4797,8 @@ public final class EmojiPagerContentComponent: Component { animationData: nil, content: .icon(.premiumStar), itemFile: nil, - subgroupId: nil + subgroupId: nil, + icon: .none ) let groupId = "recent" @@ -4782,7 +4829,8 @@ public final class EmojiPagerContentComponent: Component { animationData: animationData, content: .animation(animationData), itemFile: file, - subgroupId: nil + subgroupId: nil, + icon: .none ) if let groupIndex = itemGroupIndexById[groupId] { @@ -4809,7 +4857,8 @@ public final class EmojiPagerContentComponent: Component { animationData: animationData, content: .animation(animationData), itemFile: file, - subgroupId: nil + subgroupId: nil, + icon: .none ) if let groupIndex = itemGroupIndexById[groupId] { @@ -4831,42 +4880,38 @@ public final class EmojiPagerContentComponent: Component { switch topReaction.content { case let .builtin(value): if let reaction = availableReactions?.reactions.first(where: { $0.value == .builtin(value) }) { - topReactionItems.append(reaction) + topReactionItems.append(EmojiComponentReactionItem(reaction: .builtin(value), file: reaction.selectAnimation)) } else { continue } case let .custom(file): - topReactionItems.append(AvailableReactions.Reaction( - isEnabled: true, - isPremium: false, - value: .custom(file.fileId.id), - title: "", - staticIcon: file, - appearAnimation: file, - selectAnimation: file, - activateAnimation: file, - effectAnimation: file, - aroundAnimation: file, - centerAnimation: file - )) + topReactionItems.append(EmojiComponentReactionItem(reaction: .custom(file.fileId.id), file: file)) } } } } for reactionItem in topReactionItems { - if existingIds.contains(reactionItem.value) { + if existingIds.contains(reactionItem.reaction) { continue } - existingIds.insert(reactionItem.value) + existingIds.insert(reactionItem.reaction) - let animationFile = reactionItem.selectAnimation + let icon: EmojiPagerContentComponent.Item.Icon + if !hasPremium, case .custom = reactionItem.reaction { + icon = .locked + } else { + icon = .none + } + + let animationFile = reactionItem.file let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: animationFile, - subgroupId: nil + subgroupId: nil, + icon: icon ) let groupId = "recent" @@ -4882,41 +4927,49 @@ public final class EmojiPagerContentComponent: Component { } } + var hasRecent = false + if let recentReactions = recentReactions, !recentReactions.items.isEmpty { + hasRecent = true + } + + let maxRecentLineCount: Int if hasPremium { - var hasRecent = false - if let recentReactions = recentReactions, !recentReactions.items.isEmpty { - hasRecent = true - } - - let maxRecentLineCount: Int - #if DEBUG - maxRecentLineCount = 4 - #else maxRecentLineCount = 10 - #endif - - //TODO:localize - let popularTitle = hasRecent ? "Recently Used" : "Popular" - - if let availableReactions = availableReactions { - for reactionItem in availableReactions.reactions { - if !reactionItem.isEnabled { - continue - } - if existingIds.contains(reactionItem.value) { - continue - } - existingIds.insert(reactionItem.value) - - let animationFile = reactionItem.selectAnimation - let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: animationFile, - subgroupId: nil - ) - + } else { + maxRecentLineCount = 5 + } + + //TODO:localize + let popularTitle = hasRecent ? "Recently Used" : "Popular" + + if let availableReactions = availableReactions { + for reactionItem in availableReactions.reactions { + if !reactionItem.isEnabled { + continue + } + if existingIds.contains(reactionItem.value) { + continue + } + existingIds.insert(reactionItem.value) + + let icon: EmojiPagerContentComponent.Item.Icon + if !hasPremium, case .custom = reactionItem.value { + icon = .locked + } else { + icon = .none + } + + let animationFile = reactionItem.selectAnimation + let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: animationFile, + subgroupId: nil, + icon: icon + ) + + if hasPremium { let groupId = "popular" if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) @@ -4924,60 +4977,83 @@ public final class EmojiPagerContentComponent: Component { itemGroupIndexById[groupId] = itemGroups.count itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, isClearable: hasRecent && !isQuickReactionSelection, headerItem: nil, items: [resultItem])) } + } else { + let groupId = "recent" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + + if itemGroups[groupIndex].items.count >= maxRecentLineCount * 8 { + break + } + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, isClearable: false, headerItem: nil, items: [resultItem])) + } } } - - if let recentReactions = recentReactions { - var popularInsertIndex = 0 - for item in recentReactions.items { - guard let item = item.contents.get(RecentReactionItem.self) else { + } + + if let recentReactions = recentReactions { + var popularInsertIndex = 0 + for item in recentReactions.items { + guard let item = item.contents.get(RecentReactionItem.self) else { + continue + } + + let animationFile: TelegramMediaFile + let icon: EmojiPagerContentComponent.Item.Icon + + switch item.content { + case let .builtin(value): + if existingIds.contains(.builtin(value)) { continue } - - let animationFile: TelegramMediaFile - switch item.content { - case let .builtin(value): - if existingIds.contains(.builtin(value)) { - continue - } - existingIds.insert(.builtin(value)) - if let availableReactions = availableReactions, let availableReaction = availableReactions.reactions.first(where: { $0.value == .builtin(value) }) { - if let centerAnimation = availableReaction.centerAnimation { - animationFile = centerAnimation - } else { - continue - } + existingIds.insert(.builtin(value)) + if let availableReactions = availableReactions, let availableReaction = availableReactions.reactions.first(where: { $0.value == .builtin(value) }) { + if let centerAnimation = availableReaction.centerAnimation { + animationFile = centerAnimation } else { continue } - case let .custom(file): - if existingIds.contains(.custom(file.fileId.id)) { - continue - } - existingIds.insert(.custom(file.fileId.id)) - animationFile = file - } - - let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: animationFile, - subgroupId: nil - ) - - let groupId = "popular" - if let groupIndex = itemGroupIndexById[groupId] { - if itemGroups[groupIndex].items.count + 1 >= maxRecentLineCount * 8 { - break - } - - itemGroups[groupIndex].items.insert(resultItem, at: popularInsertIndex) - popularInsertIndex += 1 } else { - itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, isClearable: hasRecent && !isQuickReactionSelection, headerItem: nil, items: [resultItem])) + continue } + + icon = .none + case let .custom(file): + if existingIds.contains(.custom(file.fileId.id)) { + continue + } + existingIds.insert(.custom(file.fileId.id)) + animationFile = file + + if !hasPremium { + icon = .locked + } else { + icon = .none + } + } + + let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: animationFile, + subgroupId: nil, + icon: icon + ) + + let groupId = "popular" + if let groupIndex = itemGroupIndexById[groupId] { + if itemGroups[groupIndex].items.count + 1 >= maxRecentLineCount * 8 { + break + } + + itemGroups[groupIndex].items.insert(resultItem, at: popularInsertIndex) + popularInsertIndex += 1 + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, isClearable: hasRecent && !isQuickReactionSelection, headerItem: nil, items: [resultItem])) } } } @@ -5005,14 +5081,16 @@ public final class EmojiPagerContentComponent: Component { animationData: animationData, content: .animation(animationData), itemFile: file, - subgroupId: nil + subgroupId: nil, + icon: .none ) case let .text(text): resultItem = EmojiPagerContentComponent.Item( animationData: nil, content: .staticEmoji(text), itemFile: nil, - subgroupId: nil + subgroupId: nil, + icon: .none ) } @@ -5034,7 +5112,8 @@ public final class EmojiPagerContentComponent: Component { animationData: nil, content: .staticEmoji(emojiString), itemFile: nil, - subgroupId: subgroupId.rawValue + subgroupId: subgroupId.rawValue, + icon: .none ) if let groupIndex = itemGroupIndexById[groupId] { @@ -5057,12 +5136,19 @@ public final class EmojiPagerContentComponent: Component { guard let item = entry.item as? StickerPackItem else { continue } + + var icon: EmojiPagerContentComponent.Item.Icon = .none + if isReactionSelection, !hasPremium { + icon = .locked + } + let animationData = EntityKeyboardAnimationData(file: item.file) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: item.file, - subgroupId: nil + subgroupId: nil, + icon: icon ) let supergroupId = entry.index.collectionId @@ -5121,7 +5207,8 @@ public final class EmojiPagerContentComponent: Component { animationData: animationData, content: .animation(animationData), itemFile: item.file, - subgroupId: nil + subgroupId: nil, + icon: .none ) let supergroupId = featuredEmojiPack.info.id diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index 74426bb7cd..825daacf0d 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -114,7 +114,8 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { animationData: component.item, content: .animation(component.item), itemFile: nil, - subgroupId: nil + subgroupId: nil, + icon: .none ), context: component.context, attemptSynchronousLoad: false, diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index fa944cfb41..6d0eff7c3c 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1053,31 +1053,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if canAddMessageReactions(message: topMessage), let allowedReactions = allowedReactions, !topReactions.isEmpty { actions.reactionItems = topReactions.map(ReactionContextItem.reaction) - /*if hasPremiumPlaceholder && !premiumConfiguration.isPremiumDisabled { - actions.reactionItems.append(.premium) - }*/ if !actions.reactionItems.isEmpty { - let reactionItems: [AvailableReactions.Reaction] = actions.reactionItems.compactMap { item -> AvailableReactions.Reaction? in + let reactionItems: [EmojiComponentReactionItem] = actions.reactionItems.compactMap { item -> EmojiComponentReactionItem? in switch item { case let .reaction(reaction): - if let largeApplicationAnimation = reaction.largeApplicationAnimation { - return AvailableReactions.Reaction( - isEnabled: true, - isPremium: false, - value: reaction.reaction.rawValue, - title: "", - staticIcon: reaction.listAnimation, - appearAnimation: reaction.appearAnimation, - selectAnimation: reaction.stillAnimation, - activateAnimation: reaction.largeListAnimation, - effectAnimation: largeApplicationAnimation, - aroundAnimation: reaction.applicationAnimation, - centerAnimation: reaction.listAnimation - ) - } else { - return nil - } + return EmojiComponentReactionItem(reaction: reaction.reaction.rawValue, file: reaction.stillAnimation) default: return nil } diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index 1fe1cb4953..33cbf1da58 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -200,7 +200,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { animationData: animationData, content: .animation(animationData), itemFile: item.file, - subgroupId: nil + subgroupId: nil, + icon: .none ) let supergroupId = "featuredTop" @@ -236,7 +237,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { animationData: animationData, content: .animation(animationData), itemFile: item.file, - subgroupId: nil + subgroupId: nil, + icon: .none ) let groupId = "saved" @@ -263,7 +265,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { animationData: animationData, content: .animation(animationData), itemFile: item.media, - subgroupId: nil + subgroupId: nil, + icon: .none ) let groupId = "recent" @@ -313,7 +316,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { animationData: animationData, content: .animation(animationData), itemFile: item.file, - subgroupId: nil + subgroupId: nil, + icon: .none ) let groupId = "premium" @@ -345,7 +349,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { animationData: animationData, content: .animation(animationData), itemFile: item.file, - subgroupId: nil + subgroupId: nil, + icon: .none ) let groupId = "peerSpecific" @@ -367,7 +372,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { animationData: animationData, content: .animation(animationData), itemFile: item.file, - subgroupId: nil + subgroupId: nil, + icon: .none ) let groupId = entry.index.collectionId if let groupIndex = itemGroupIndexById[groupId] { @@ -419,7 +425,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { animationData: animationData, content: .animation(animationData), itemFile: item.file, - subgroupId: nil + subgroupId: nil, + icon: .none ) let supergroupId = featuredStickerPack.info.id diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 34431cf78a..4180590a27 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -211,12 +211,14 @@ private func canViewReadStats(message: Message, participantCount: Int?, isMessag case let channel as TelegramChannel: if case .broadcast = channel.info { return false - } else if let participantCount = participantCount { - if participantCount > maxParticipantCount { + } else { + if let participantCount = participantCount { + if participantCount > maxParticipantCount { + return false + } + } else { return false } - } else { - return false } case let group as TelegramGroup: if group.participantCount > maxParticipantCount { @@ -1571,19 +1573,17 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState reactionCount = 0 } - var readStats = readStats + /*var readStats = readStats if !canViewStats { readStats = MessageReadStats(reactionCount: 0, peers: []) - } - - let reactionCount = readStats?.reactionCount ?? 0 + }*/ if hasReadReports || reactionCount != 0 { if !actions.isEmpty { actions.insert(.separator, at: 0) } - actions.insert(.custom(ChatReadReportContextItem(context: context, message: message, stats: readStats, action: { c, f, stats, customReactionEmojiPacks, firstCustomEmojiReaction in + actions.insert(.custom(ChatReadReportContextItem(context: context, message: message, hasReadReports: hasReadReports, stats: readStats, action: { c, f, stats, customReactionEmojiPacks, firstCustomEmojiReaction in if reactionCount == 0, let stats = stats, stats.peers.count == 1 { c.dismiss(completion: { controllerInteraction.openPeer(stats.peers[0].id, .default, nil, false, nil) @@ -2162,12 +2162,14 @@ private final class ChatDeleteMessageContextItemNode: ASDisplayNode, ContextMenu final class ChatReadReportContextItem: ContextMenuCustomItem { fileprivate let context: AccountContext fileprivate let message: Message + fileprivate let hasReadReports: Bool fileprivate let stats: MessageReadStats? fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats?, [StickerPackCollectionInfo], TelegramMediaFile?) -> Void - init(context: AccountContext, message: Message, stats: MessageReadStats?, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats?, [StickerPackCollectionInfo], TelegramMediaFile?) -> Void) { + init(context: AccountContext, message: Message, hasReadReports: Bool, stats: MessageReadStats?, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats?, [StickerPackCollectionInfo], TelegramMediaFile?) -> Void) { self.context = context self.message = message + self.hasReadReports = hasReadReports self.stats = stats self.action = action } @@ -2380,7 +2382,7 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus private var validLayout: (calculatedWidth: CGFloat, size: CGSize)? func updateStats(stats: MessageReadStats, transition: ContainedViewLayoutTransition) { - self.buttonNode.isUserInteractionEnabled = !stats.peers.isEmpty + self.buttonNode.isUserInteractionEnabled = !stats.peers.isEmpty || stats.reactionCount != 0 guard let (calculatedWidth, size) = self.validLayout else { return diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 8da63bbb5b..d02e8e19d6 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -1769,6 +1769,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate private let cachedFaq = Promise(nil) private weak var copyProtectionTooltipController: TooltipController? + weak var emojiStatusSelectionController: ViewController? private let _ready = Promise() var ready: Promise { @@ -3103,7 +3104,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let animationCache = context.animationCache let animationRenderer = context.animationRenderer - strongSelf.controller?.present(EmojiStatusSelectionController( + strongSelf.emojiStatusSelectionController?.dismiss() + let emojiStatusSelectionController = EmojiStatusSelectionController( context: strongSelf.context, mode: .statusSelection, sourceView: sourceView, @@ -3122,7 +3124,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate destinationItemView: { [weak sourceView] in return sourceView } - ), in: .window(.root)) + ) + strongSelf.emojiStatusSelectionController = emojiStatusSelectionController + strongSelf.controller?.present(emojiStatusSelectionController, 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) @@ -8307,6 +8311,11 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc super.viewWillDisappear(animated) self.dismissAllTooltips() + + if let emojiStatusSelectionController = self.controllerNode.emojiStatusSelectionController { + self.controllerNode.emojiStatusSelectionController = nil + emojiStatusSelectionController.dismiss() + } } override public func viewDidAppear(_ animated: Bool) { diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index eca1024b52..7e02beeba8 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -51,6 +51,11 @@ public enum UndoOverlayAction { } public final class UndoOverlayController: ViewController { + public enum Position { + case top + case bottom + } + private let presentationData: PresentationData public var content: UndoOverlayContent { didSet { @@ -58,6 +63,7 @@ public final class UndoOverlayController: ViewController { } } private let elevatedLayout: Bool + private let position: Position private let animateInAsReplacement: Bool private var action: (UndoOverlayAction) -> Bool @@ -66,10 +72,11 @@ public final class UndoOverlayController: ViewController { public var keepOnParentDismissal = false - public init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, animateInAsReplacement: Bool = false, action: @escaping (UndoOverlayAction) -> Bool) { + public init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, position: Position = .bottom, animateInAsReplacement: Bool = false, action: @escaping (UndoOverlayAction) -> Bool) { self.presentationData = presentationData self.content = content self.elevatedLayout = elevatedLayout + self.position = position self.animateInAsReplacement = animateInAsReplacement self.action = action @@ -83,7 +90,7 @@ public final class UndoOverlayController: ViewController { } override public func loadDisplayNode() { - self.displayNode = UndoOverlayControllerNode(presentationData: self.presentationData, content: self.content, elevatedLayout: self.elevatedLayout, action: { [weak self] value in + self.displayNode = UndoOverlayControllerNode(presentationData: self.presentationData, content: self.content, elevatedLayout: self.elevatedLayout, placementPosition: self.position, action: { [weak self] value in return self?.action(value) ?? false }, dismiss: { [weak self] in self?.dismiss() diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 9915f8fae4..85b04bf44e 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -20,6 +20,7 @@ import AccountContext final class UndoOverlayControllerNode: ViewControllerTracingNode { private let elevatedLayout: Bool + private let placementPosition: UndoOverlayController.Position private var statusNode: RadialStatusNode? private let timerTextNode: ImmediateTextNode private let avatarNode: AvatarNode? @@ -56,8 +57,9 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { private var fetchResourceDisposable: Disposable? - init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, action: @escaping (UndoOverlayAction) -> Bool, dismiss: @escaping () -> Void) { + init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, placementPosition: UndoOverlayController.Position, action: @escaping (UndoOverlayAction) -> Bool, dismiss: @escaping () -> Void) { self.elevatedLayout = elevatedLayout + self.placementPosition = placementPosition self.content = content self.action = action @@ -1099,12 +1101,23 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { contentHeight = max(49.0, contentHeight) var insets = layout.insets(options: [.input]) - if self.elevatedLayout { - insets.bottom += 49.0 + switch self.placementPosition { + case .top: + insets.top = layout.statusBarHeight ?? 0.0 + case .bottom: + if self.elevatedLayout { + insets.bottom += 49.0 + } + } + + var panelFrame = CGRect(origin: CGPoint(x: margin + layout.safeInsets.left, y: layout.size.height - contentHeight - insets.bottom - margin), size: CGSize(width: layout.size.width - margin * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: contentHeight)) + var panelWrapperFrame = CGRect(origin: CGPoint(x: margin + layout.safeInsets.left, y: layout.size.height - contentHeight - insets.bottom - margin), size: CGSize(width: layout.size.width - margin * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: contentHeight)) + + if case .top = self.placementPosition { + panelFrame.origin.y = insets.top + panelWrapperFrame.origin.y = insets.top } - let panelFrame = CGRect(origin: CGPoint(x: margin + layout.safeInsets.left, y: layout.size.height - contentHeight - insets.bottom - margin), size: CGSize(width: layout.size.width - margin * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: contentHeight)) - let panelWrapperFrame = CGRect(origin: CGPoint(x: margin + layout.safeInsets.left, y: layout.size.height - contentHeight - insets.bottom - margin), size: CGSize(width: layout.size.width - margin * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: contentHeight)) transition.updateFrame(node: self.panelNode, frame: panelFrame) transition.updateFrame(node: self.panelWrapperNode, frame: panelWrapperFrame) self.effectView.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width - margin * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: contentHeight)