diff --git a/Telegram/Telegram-iOS/main.m b/Telegram/Telegram-iOS/main.m index dcc092613b..81d976a847 100644 --- a/Telegram/Telegram-iOS/main.m +++ b/Telegram/Telegram-iOS/main.m @@ -1,16 +1,6 @@ #import -#import int main(int argc, char *argv[]) { - /*NSString *basePath = [[NSString stringWithUTF8String:argv[0]] stringByDeletingLastPathComponent]; - void *Share = dlopen([[basePath stringByAppendingPathComponent:@"PlugIns/Share.appex/Share"] UTF8String], RTLD_LAZY); - void *NotificationContent = dlopen([[basePath stringByAppendingPathComponent:@"PlugIns/NotificationContent.appex/NotificationContent"] UTF8String], RTLD_LAZY); - sleep(1000); - void *NotificationService = dlopen([[basePath stringByAppendingPathComponent:@"PlugIns/NotificationService.appex/NotificationService"] UTF8String], RTLD_LAZY); - void *SiriIntents = dlopen([[basePath stringByAppendingPathComponent:@"PlugIns/SiriIntents.appex/SiriIntents"] UTF8String], RTLD_LAZY); - void *Widget = dlopen([[basePath stringByAppendingPathComponent:@"PlugIns/Widget.appex/Widget"] UTF8String], RTLD_LAZY); - 1*/ - @autoreleasepool { return UIApplicationMain(argc, argv, @"Application", @"AppDelegate"); } diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift index b38b9de3bd..726be53896 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift @@ -666,11 +666,11 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati panelControlColor: UIColor(rgb: 0xffffff), panelControlDisabledColor: UIColor(rgb: 0x808080, alpha: 0.5), panelControlDestructiveColor: UIColor(rgb: 0xff3b30), - inputBackgroundColor: UIColor(rgb: 0x060606), + inputBackgroundColor: UIColor(rgb: 0xffffff, alpha: 0.14).blitOver(.black, alpha: 1.0).withAlphaComponent(0.95), inputStrokeColor: UIColor(rgb: 0xffffff, alpha: 0.1), inputPlaceholderColor: UIColor(rgb: 0x7b7b7b), inputTextColor: UIColor(rgb: 0xffffff), - inputControlColor: UIColor(rgb: 0xffffff), + inputControlColor: UIColor(rgb: 0xffffff, alpha: 0.5), actionControlFillColor: UIColor(rgb: 0xffffff), actionControlForegroundColor: UIColor(rgb: 0x000000), primaryTextColor: UIColor(rgb: 0xffffff), diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index 5bfbee3634..d4a76f0d48 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -950,7 +950,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio inputStrokeColor: UIColor(rgb: 0x000000, alpha: 0.1), inputPlaceholderColor: UIColor(rgb: 0x000000, alpha: 0.4), inputTextColor: UIColor(rgb: 0x000000), - inputControlColor: UIColor(rgb: 0x000000, alpha: 1.0), + inputControlColor: UIColor(rgb: 0x000000, alpha: 0.6), actionControlFillColor: defaultDayAccentColor, actionControlForegroundColor: UIColor(rgb: 0xffffff), primaryTextColor: UIColor(rgb: 0x000000, alpha: 1.0), diff --git a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift index a94bd33a69..e13343e082 100644 --- a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift @@ -400,13 +400,13 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { if previousState?.theme !== interfaceState.theme { self.helpButtonIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/Help"), color: .white)?.withRenderingMode(.alwaysTemplate) - self.helpButtonIconView.tintColor = interfaceState.theme.chat.inputPanel.inputControlColor + self.helpButtonIconView.tintColor = interfaceState.theme.chat.inputPanel.panelControlColor self.suggestedPostButtonIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/SuggestPost"), color: .white)?.withRenderingMode(.alwaysTemplate) - self.suggestedPostButtonIconView.tintColor = interfaceState.theme.chat.inputPanel.inputControlColor + self.suggestedPostButtonIconView.tintColor = interfaceState.theme.chat.inputPanel.panelControlColor self.giftButtonIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/Gift"), color: .white)?.withRenderingMode(.alwaysTemplate) - self.giftButtonIconView.tintColor = interfaceState.theme.chat.inputPanel.inputControlColor + self.giftButtonIconView.tintColor = interfaceState.theme.chat.inputPanel.panelControlColor } if let context = self.context, let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) || previousState?.theme !== interfaceState.theme || previousState?.strings !== interfaceState.strings || previousState?.peerIsMuted != interfaceState.peerIsMuted || previousState?.pinnedMessage != interfaceState.pinnedMessage || force { @@ -441,7 +441,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { if case .join = self.action { titleColor = interfaceState.theme.chat.inputPanel.actionControlForegroundColor } else { - titleColor = interfaceState.theme.chat.inputPanel.inputControlColor + titleColor = interfaceState.theme.chat.inputPanel.panelControlColor } self.buttonTitle.attributedText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: titleColor) self.buttonTintTitle.attributedText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: .black) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift index 89a2d43fb0..510bb9cae0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift @@ -149,7 +149,7 @@ private final class GlassButtonView: HighlightTrackingButton { func update(theme: PresentationTheme, size: CGSize, transition: ComponentTransition) { let params = Params(theme: theme, size: size) if self.params != params { - self.iconView.tintColor = params.theme.chat.inputPanel.inputControlColor + self.iconView.tintColor = params.theme.chat.inputPanel.panelControlColor self.params = params self.updateImpl(params: params, transition: transition) } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode/Sources/ChatRecordingPreviewInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode/Sources/ChatRecordingPreviewInputPanelNode.swift index 68021514cc..2b467ec9c0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode/Sources/ChatRecordingPreviewInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode/Sources/ChatRecordingPreviewInputPanelNode.swift @@ -415,7 +415,7 @@ public final class ChatRecordingPreviewInputPanelNodeImpl: ChatInputPanelNode { } if updateWaveform { - self.waveformNode.waveformNode.setup(color: interfaceState.theme.chat.inputPanel.inputControlColor.withMultipliedAlpha(0.4), gravity: .center, waveform: audio.waveform) + self.waveformNode.waveformNode.setup(color: interfaceState.theme.chat.inputPanel.panelControlColor.withMultipliedAlpha(0.4), gravity: .center, waveform: audio.waveform) self.waveformNode.foregroundWaveformNode.setup(color: interfaceState.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.5), gravity: .center, waveform: audio.waveform) self.tintWaveformNode.setup(color: UIColor(white: 0.0, alpha: 0.5), gravity: .center, waveform: audio.waveform) self.waveformForegroundNode.setup(color: interfaceState.theme.list.itemCheckColors.foregroundColor, gravity: .center, waveform: audio.waveform) diff --git a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/BUILD b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/BUILD index faaa994060..626e7fbeba 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/BUILD @@ -31,6 +31,7 @@ swift_library( "//submodules/TelegramUI/Components/MaskedContainerComponent", "//submodules/AppBundle", "//submodules/PresentationDataUtils", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatFloatingTopicsPanel.swift b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatFloatingTopicsPanel.swift new file mode 100644 index 0000000000..820e9376de --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatFloatingTopicsPanel.swift @@ -0,0 +1,2316 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import Display +import AsyncDisplayKit +import TelegramPresentationData +import ChatPresentationInterfaceState +import ComponentFlow +import MultilineTextComponent +import AccountContext +import BlurredBackgroundComponent +import EmojiStatusComponent +import BundleIconComponent +import AvatarNode +import ChatListUI +import ContextUI +import AsyncListComponent +import TextBadgeComponent +import MaskedContainerComponent +import AppBundle +import PresentationDataUtils + +/*public final class ChatFloatingTopicsPanel: Component { + public enum Location { + case side + case top + } + + public enum Kind { + case forum + case monoforum + case botForum + } + + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let location: Location + let peerId: EnginePeer.Id + let kind: Kind + let topicId: Int64? + let controller: () -> ViewController? + let togglePanel: () -> Void + let updateTopicId: (Int64?, Bool) -> Void + let openDeletePeer: (Int64) -> Void + + public init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + location: Location, + peerId: EnginePeer.Id, + kind: Kind, + topicId: Int64?, + controller: @escaping () -> ViewController?, + togglePanel: @escaping () -> Void, + updateTopicId: @escaping (Int64?, Bool) -> Void, + openDeletePeer: @escaping (Int64) -> Void + ) { + self.context = context + self.theme = theme + self.strings = strings + self.location = location + self.peerId = peerId + self.kind = kind + self.topicId = topicId + self.controller = controller + self.togglePanel = togglePanel + self.updateTopicId = updateTopicId + self.openDeletePeer = openDeletePeer + } + + public static func ==(lhs: ChatFloatingTopicsPanel, rhs: ChatFloatingTopicsPanel) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.location != rhs.location { + return false + } + if lhs.peerId != rhs.peerId { + return false + } + if lhs.kind != rhs.kind { + return false + } + if lhs.topicId != rhs.topicId { + return false + } + return true + } + + private final class Item: Equatable { + typealias Id = EngineChatList.Item.Id + + let item: EngineChatList.Item + + var id: Id { + return self.item.id + } + + init(item: EngineChatList.Item) { + self.item = item + } + + public static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs === rhs { + return true + } + if lhs.item != rhs.item { + return false + } + return true + } + } + + private protocol ItemComponent: AnyObject { + var item: Item { get } + } + + private final class VerticalItemComponent: Component, ItemComponent { + let context: AccountContext + let item: Item + let isSelected: Bool + let isReordering: Bool + let theme: PresentationTheme + let action: (() -> Void)? + let contextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)? + + init(context: AccountContext, item: Item, isSelected: Bool, isReordering: Bool, theme: PresentationTheme, strings: PresentationStrings, action: (() -> Void)?, contextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)?) { + self.context = context + self.item = item + self.isSelected = isSelected + self.isReordering = isReordering + self.theme = theme + self.action = action + self.contextGesture = contextGesture + } + + static func ==(lhs: VerticalItemComponent, rhs: VerticalItemComponent) -> Bool { + if lhs === rhs { + return true + } + if lhs.context !== rhs.context { + return false + } + if lhs.item != rhs.item { + return false + } + if lhs.isSelected != rhs.isSelected { + return false + } + if lhs.isReordering != rhs.isReordering { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if (lhs.action == nil) != (rhs.action == nil) { + return false + } + if (lhs.contextGesture == nil) != (rhs.contextGesture == nil) { + return false + } + return true + } + + final class View: UIView, AsyncListComponent.ItemView { + private let extractedContainerNode: ContextExtractedContentContainingNode + private let containerNode: ContextControllerSourceNode + + private let containerButton: UIView + private var extractedBackgroundView: UIImageView? + + private var tapRecognizer: UITapGestureRecognizer? + + private let iconContainer: MaskedContainerView + private var icon: ComponentView? + private var avatarNode: AvatarNode? + private let title = ComponentView() + private var badge: ComponentView? + + private var component: VerticalItemComponent? + + override init(frame: CGRect) { + self.extractedContainerNode = ContextExtractedContentContainingNode() + self.containerNode = ContextControllerSourceNode() + + self.iconContainer = MaskedContainerView() + self.iconContainer.isUserInteractionEnabled = false + + self.containerButton = UIView() + + super.init(frame: frame) + + self.extractedContainerNode.contentNode.view.addSubview(self.containerButton) + + self.containerNode.addSubnode(self.extractedContainerNode) + self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode + self.addSubview(self.containerNode.view) + + self.containerButton.addSubview(self.iconContainer) + + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + self.tapRecognizer = tapRecognizer + self.containerButton.addGestureRecognizer(tapRecognizer) + tapRecognizer.isEnabled = false + + self.containerNode.activated = { [weak self] gesture, _ in + guard let self, let component = self.component else { + return + } + component.contextGesture?(gesture, self.extractedContainerNode) + } + + self.extractedContainerNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in + guard let self, let component = self.component else { + return + } + + if isExtracted { + let extractedBackgroundView: UIImageView + if let current = self.extractedBackgroundView { + extractedBackgroundView = current + } else { + extractedBackgroundView = UIImageView(image: generateStretchableFilledCircleImage(diameter: 28.0, color: component.theme.contextMenu.backgroundColor)) + self.extractedBackgroundView = extractedBackgroundView + self.extractedContainerNode.contentNode.view.insertSubview(extractedBackgroundView, at: 0) + extractedBackgroundView.frame = self.extractedContainerNode.contentNode.bounds.insetBy(dx: 2.0, dy: 0.0) + extractedBackgroundView.alpha = 0.0 + } + transition.updateAlpha(layer: extractedBackgroundView.layer, alpha: 1.0) + } else if let extractedBackgroundView = self.extractedBackgroundView { + self.extractedBackgroundView = nil + let alphaTransition: ContainedViewLayoutTransition + if transition.isAnimated { + alphaTransition = .animated(duration: 0.18, curve: .easeInOut) + } else { + alphaTransition = .immediate + } + alphaTransition.updateAlpha(layer: extractedBackgroundView.layer, alpha: 0.0, completion: { [weak extractedBackgroundView] _ in + extractedBackgroundView?.removeFromSuperview() + }) + } + } + + self.containerNode.isGestureEnabled = false + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + if let iconView = self.icon?.view as? EmojiStatusComponent.View { + iconView.playOnce() + } + self.component?.action?() + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + var mappedPoint = point + if self.bounds.insetBy(dx: -8.0, dy: -4.0).contains(point) { + mappedPoint = self.bounds.center + } + return super.hitTest(mappedPoint, with: event) + } + + func isReorderable(at point: CGPoint) -> Bool { + guard let component = self.component else { + return false + } + return component.isReordering + } + + private func updateIsShaking(animated: Bool) { + guard let component = self.component else { + return + } + + if component.isReordering { + if self.layer.animation(forKey: "shaking_position") == nil { + let degreesToRadians: (_ x: CGFloat) -> CGFloat = { x in + return .pi * x / 180.0 + } + + let duration: Double = 0.4 + let displacement: CGFloat = 1.0 + let degreesRotation: CGFloat = 2.0 + + let negativeDisplacement = -1.0 * displacement + let position = CAKeyframeAnimation.init(keyPath: "position") + position.beginTime = 0.8 + position.duration = duration + position.values = [ + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)), + NSValue(cgPoint: CGPoint(x: 0, y: 0)), + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)), + NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)), + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)) + ] + position.calculationMode = .linear + position.isRemovedOnCompletion = false + position.repeatCount = Float.greatestFiniteMagnitude + position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100)) + position.isAdditive = true + + let transform = CAKeyframeAnimation.init(keyPath: "transform") + transform.beginTime = 2.6 + transform.duration = 0.3 + transform.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ) + transform.values = [ + degreesToRadians(-1.0 * degreesRotation), + degreesToRadians(degreesRotation), + degreesToRadians(-1.0 * degreesRotation) + ] + transform.calculationMode = .linear + transform.isRemovedOnCompletion = false + transform.repeatCount = Float.greatestFiniteMagnitude + transform.isAdditive = true + transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100)) + + self.layer.add(position, forKey: "shaking_position") + self.layer.add(transform, forKey: "shaking_rotation") + } + } else if self.layer.animation(forKey: "shaking_position") != nil { + if let presentationLayer = self.layer.presentation() { + let transition: ComponentTransition = .easeInOut(duration: 0.1) + if presentationLayer.position != self.layer.position { + transition.animatePosition(layer: self.layer, from: CGPoint(x: presentationLayer.position.x - self.layer.position.x, y: presentationLayer.position.y - self.layer.position.y), to: CGPoint(), additive: true) + } + if !CATransform3DIsIdentity(presentationLayer.transform) { + transition.setTransform(layer: self.layer, transform: CATransform3DIdentity) + } + } + + self.layer.removeAnimation(forKey: "shaking_position") + self.layer.removeAnimation(forKey: "shaking_rotation") + } + } + + func update(component: VerticalItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let previousComponent = self.component + self.component = component + + self.tapRecognizer?.isEnabled = component.action != nil + + self.containerNode.isGestureEnabled = component.contextGesture != nil + self.containerNode.activated = { [weak self] gesture, _ in + guard let self, let component = self.component else { + return + } + component.contextGesture?(gesture, self.extractedContainerNode) + } + + let topInset: CGFloat = 8.0 + let bottomInset: CGFloat = 8.0 + let spacing: CGFloat = 3.0 + let iconSize = CGSize(width: 30.0, height: 30.0) + + var avatarIconContent: EmojiStatusComponent.Content? + if case let .forum(topicId) = component.item.item.id { + if topicId != 1, let threadData = component.item.item.threadData { + if let fileId = threadData.info.icon, fileId != 0 { + avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: iconSize, placeholderColor: component.theme.list.mediaPlaceholderColor, themeColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.controlColor, loopMode: .count(0)) + } else { + avatarIconContent = .topic(title: String(threadData.info.title.prefix(1)), color: threadData.info.iconColor, size: iconSize) + } + } else { + //newTopicTemplateIcon + avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicTemplateIcon(component.theme), tintColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.controlColor) + } + } + + if let avatarIconContent { + let avatarIconComponent = EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: avatarIconContent, + isVisibleForAnimations: true, + action: nil + ) + let icon: ComponentView + if let current = self.icon { + icon = current + } else { + icon = ComponentView() + self.icon = icon + } + + var iconTransition = transition + if iconTransition.animation.isImmediate, let previousComponent, previousComponent.isSelected != component.isSelected { + iconTransition = .easeInOut(duration: 0.2) + } + + let _ = icon.update( + transition: iconTransition, + component: AnyComponent(avatarIconComponent), + environment: {}, + containerSize: iconSize + ) + } else if let icon = self.icon { + self.icon = nil + icon.view?.removeFromSuperview() + } + + let titleText: String + if case let .forum(topicId) = component.item.item.id { + let _ = topicId + if let threadData = component.item.item.threadData { + titleText = threadData.info.title + } else { + titleText = " " + } + } else { + titleText = component.item.item.renderedPeer.chatMainPeer?.compactDisplayTitle ?? " " + } + + if let avatarIconContent, let icon = self.icon { + let avatarIconComponent = EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: avatarIconContent, + isVisibleForAnimations: true, + action: nil + ) + let _ = icon.update( + transition: .immediate, + component: AnyComponent(avatarIconComponent), + environment: {}, + containerSize: iconSize + ) + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleText, font: Font.regular(10.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.secondaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 6.0 * 2.0, height: 100.0) + ) + + let contentSize: CGFloat = topInset + bottomInset + iconSize.height + spacing + titleSize.height + let size = CGSize(width: availableSize.width, height: contentSize) + + let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) * 0.5), y: topInset), size: iconSize) + let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: iconFrame.maxY + spacing), size: titleSize) + + self.iconContainer.frame = iconFrame + + if let icon = self.icon { + if let avatarNode = self.avatarNode { + self.avatarNode = nil + avatarNode.view.removeFromSuperview() + } + + if let iconView = icon.view { + if iconView.superview == nil { + iconView.isUserInteractionEnabled = false + self.iconContainer.contentView.addSubview(iconView) + } + iconView.frame = CGRect(origin: CGPoint(), size: iconFrame.size) + } + } else { + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 11.0)) + avatarNode.isUserInteractionEnabled = false + self.avatarNode = avatarNode + self.iconContainer.contentView.addSubview(avatarNode.view) + } + avatarNode.frame = CGRect(origin: CGPoint(), size: iconFrame.size) + avatarNode.updateSize(size: iconFrame.size) + + if let peer = component.item.item.renderedPeer.chatMainPeer { + if peer.smallProfileImage != nil { + avatarNode.setPeerV2(context: component.context, theme: component.theme, peer: peer, overrideImage: nil, emptyColor: .gray, clipStyle: .round, synchronousLoad: false, displayDimensions: iconFrame.size) + } else { + avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, overrideImage: nil, emptyColor: .gray, clipStyle: .round, synchronousLoad: false, displayDimensions: iconFrame.size) + } + } + } + + var iconMaskItems: [MaskedContainerView.Item] = [] + if let readCounters = component.item.item.readCounters, readCounters.count > 0 { + let badge: ComponentView + var badgeTransition = transition + if let current = self.badge { + badge = current + } else { + badgeTransition = .immediate + badge = ComponentView() + self.badge = badge + } + + let badgeSize = badge.update( + transition: badgeTransition, + component: AnyComponent(TextBadgeComponent( + text: countString(Int64(readCounters.count)), + font: Font.medium(12.0), + background: component.item.item.isMuted ? component.theme.chatList.unreadBadgeInactiveBackgroundColor : component.theme.chatList.unreadBadgeActiveBackgroundColor, + foreground: component.item.item.isMuted ? component.theme.chatList.unreadBadgeInactiveTextColor : component.theme.chatList.unreadBadgeActiveTextColor, + insets: UIEdgeInsets(top: 1.0, left: 5.0, bottom: 2.0, right: 5.0) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let badgeFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + 10.0 - badgeSize.width, y: iconFrame.minY - 6.0), size: badgeSize) + if let badgeView = badge.view { + if badgeView.superview == nil { + self.containerButton.addSubview(badgeView) + } + badgeView.frame = badgeFrame + } + let badgeMaskFrame = badgeFrame.offsetBy(dx: -iconFrame.minX, dy: -iconFrame.minY).insetBy(dx: -1.33, dy: -1.33) + iconMaskItems.append(MaskedContainerView.Item( + frame: badgeMaskFrame, + shape: .roundedRect(cornerRadius: badgeMaskFrame.height * 0.5) + )) + } else if let badge = self.badge { + self.badge = nil + badge.view?.removeFromSuperview() + } + self.iconContainer.update(size: iconFrame.size, items: iconMaskItems, isInverted: true) + self.iconContainer.frame = iconFrame + + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.containerButton.addSubview(titleView) + } + titleView.frame = titleFrame + } + + transition.setFrame(view: self.containerButton, frame: CGRect(origin: CGPoint(), size: size)) + + self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size) + self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size) + self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) + self.containerNode.frame = CGRect(origin: CGPoint(), size: size) + + self.updateIsShaking(animated: !transition.animation.isImmediate) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } + } + + private final class HorizontalItemComponent: Component, ItemComponent { + let context: AccountContext + let item: Item + let isSelected: Bool + let isReordering: Bool + let theme: PresentationTheme + let action: (() -> Void)? + let contextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)? + + init(context: AccountContext, item: Item, isSelected: Bool, isReordering: Bool, theme: PresentationTheme, strings: PresentationStrings, action: (() -> Void)?, contextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)?) { + self.context = context + self.item = item + self.isSelected = isSelected + self.isReordering = isReordering + self.theme = theme + self.action = action + self.contextGesture = contextGesture + } + + static func ==(lhs: HorizontalItemComponent, rhs: HorizontalItemComponent) -> Bool { + if lhs === rhs { + return true + } + if lhs.context !== rhs.context { + return false + } + if lhs.item != rhs.item { + return false + } + if lhs.isSelected != rhs.isSelected { + return false + } + if lhs.isReordering != rhs.isReordering { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if (lhs.action == nil) != (rhs.action == nil) { + return false + } + if (lhs.contextGesture == nil) != (rhs.contextGesture == nil) { + return false + } + return true + } + + final class View: UIView, AsyncListComponent.ItemView { + private let extractedContainerNode: ContextExtractedContentContainingNode + private let containerNode: ContextControllerSourceNode + + private let containerButton: UIView + private var extractedBackgroundView: UIImageView? + + private var tapRecognizer: UITapGestureRecognizer? + + private var icon: ComponentView? + private var avatarNode: AvatarNode? + private let title = ComponentView() + private var badge: ComponentView? + + private var component: HorizontalItemComponent? + + override init(frame: CGRect) { + self.extractedContainerNode = ContextExtractedContentContainingNode() + self.containerNode = ContextControllerSourceNode() + + self.containerButton = UIView() + + super.init(frame: frame) + + self.extractedContainerNode.contentNode.view.addSubview(self.containerButton) + + self.containerNode.addSubnode(self.extractedContainerNode) + self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode + self.addSubview(self.containerNode.view) + + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + self.tapRecognizer = tapRecognizer + self.containerButton.addGestureRecognizer(tapRecognizer) + tapRecognizer.isEnabled = false + + self.containerNode.activated = { [weak self] gesture, _ in + guard let self, let component = self.component else { + return + } + component.contextGesture?(gesture, self.extractedContainerNode) + } + + self.extractedContainerNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in + guard let self, let component = self.component else { + return + } + + if isExtracted { + let extractedBackgroundView: UIImageView + if let current = self.extractedBackgroundView { + extractedBackgroundView = current + } else { + extractedBackgroundView = UIImageView(image: generateStretchableFilledCircleImage(diameter: 28.0, color: component.theme.contextMenu.backgroundColor)) + self.extractedBackgroundView = extractedBackgroundView + self.extractedContainerNode.contentNode.view.insertSubview(extractedBackgroundView, at: 0) + extractedBackgroundView.frame = self.extractedContainerNode.contentNode.bounds.insetBy(dx: 2.0, dy: 0.0) + extractedBackgroundView.alpha = 0.0 + } + transition.updateAlpha(layer: extractedBackgroundView.layer, alpha: 1.0) + } else if let extractedBackgroundView = self.extractedBackgroundView { + self.extractedBackgroundView = nil + let alphaTransition: ContainedViewLayoutTransition + if transition.isAnimated { + alphaTransition = .animated(duration: 0.18, curve: .easeInOut) + } else { + alphaTransition = .immediate + } + alphaTransition.updateAlpha(layer: extractedBackgroundView.layer, alpha: 0.0, completion: { [weak extractedBackgroundView] _ in + extractedBackgroundView?.removeFromSuperview() + }) + } + } + + self.containerNode.isGestureEnabled = false + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + if let iconView = self.icon?.view as? EmojiStatusComponent.View { + iconView.playOnce() + } + self.component?.action?() + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + var mappedPoint = point + if self.bounds.insetBy(dx: -8.0, dy: -4.0).contains(point) { + mappedPoint = self.bounds.center + } + return super.hitTest(mappedPoint, with: event) + } + + func isReorderable(at point: CGPoint) -> Bool { + guard let component = self.component else { + return false + } + return component.isReordering + } + + private func updateIsShaking(animated: Bool) { + guard let component = self.component else { + return + } + + if component.isReordering { + if self.layer.animation(forKey: "shaking_position") == nil { + let degreesToRadians: (_ x: CGFloat) -> CGFloat = { x in + return .pi * x / 180.0 + } + + let duration: Double = 0.4 + let displacement: CGFloat = 1.0 + let degreesRotation: CGFloat = 2.0 + + let negativeDisplacement = -1.0 * displacement + let position = CAKeyframeAnimation.init(keyPath: "position") + position.beginTime = 0.8 + position.duration = duration + position.values = [ + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)), + NSValue(cgPoint: CGPoint(x: 0, y: 0)), + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)), + NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)), + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)) + ] + position.calculationMode = .linear + position.isRemovedOnCompletion = false + position.repeatCount = Float.greatestFiniteMagnitude + position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100)) + position.isAdditive = true + + let transform = CAKeyframeAnimation.init(keyPath: "transform") + transform.beginTime = 2.6 + transform.duration = 0.3 + transform.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ) + transform.values = [ + degreesToRadians(-1.0 * degreesRotation), + degreesToRadians(degreesRotation), + degreesToRadians(-1.0 * degreesRotation) + ] + transform.calculationMode = .linear + transform.isRemovedOnCompletion = false + transform.repeatCount = Float.greatestFiniteMagnitude + transform.isAdditive = true + transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100)) + + self.layer.add(position, forKey: "shaking_position") + self.layer.add(transform, forKey: "shaking_rotation") + } + } else if self.layer.animation(forKey: "shaking_position") != nil { + if let presentationLayer = self.layer.presentation() { + let transition: ComponentTransition = .easeInOut(duration: 0.1) + if presentationLayer.position != self.layer.position { + transition.animatePosition(layer: self.layer, from: CGPoint(x: presentationLayer.position.x - self.layer.position.x, y: presentationLayer.position.y - self.layer.position.y), to: CGPoint(), additive: true) + } + if !CATransform3DIsIdentity(presentationLayer.transform) { + transition.setTransform(layer: self.layer, transform: CATransform3DIdentity) + } + } + + self.layer.removeAnimation(forKey: "shaking_position") + self.layer.removeAnimation(forKey: "shaking_rotation") + } + } + + func update(component: HorizontalItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + self.tapRecognizer?.isEnabled = component.action != nil + + self.containerNode.isGestureEnabled = component.contextGesture != nil + self.containerNode.activated = { [weak self] gesture, _ in + guard let self, let component = self.component else { + return + } + component.contextGesture?(gesture, self.extractedContainerNode) + } + + let leftInset: CGFloat = 12.0 + let rightInset: CGFloat = 12.0 + let spacing: CGFloat = 4.0 + let badgeSpacing: CGFloat = 4.0 + let iconSize = CGSize(width: 18.0, height: 18.0) + + var avatarIconContent: EmojiStatusComponent.Content? + if case let .forum(topicId) = component.item.item.id { + if topicId != 1, let threadData = component.item.item.threadData { + if let fileId = threadData.info.icon, fileId != 0 { + avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: iconSize, placeholderColor: component.theme.list.mediaPlaceholderColor, themeColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.controlColor, loopMode: .count(0)) + } else { + avatarIconContent = .topic(title: String(threadData.info.title.prefix(1)), color: threadData.info.iconColor, size: iconSize) + } + } else { + //newTopicTemplateIcon + avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicTemplateIcon(component.theme), tintColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.controlColor) + } + } + + if let avatarIconContent { + let avatarIconComponent = EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: avatarIconContent, + isVisibleForAnimations: false, + action: nil + ) + let icon: ComponentView + if let current = self.icon { + icon = current + } else { + icon = ComponentView() + self.icon = icon + } + let _ = icon.update( + transition: .immediate, + component: AnyComponent(avatarIconComponent), + environment: {}, + containerSize: iconSize + ) + } else if let icon = self.icon { + self.icon = nil + icon.view?.removeFromSuperview() + } + + let titleText: String + if case let .forum(topicId) = component.item.item.id { + let _ = topicId + if let threadData = component.item.item.threadData { + titleText = threadData.info.title + } else { + titleText = " " + } + } else { + titleText = component.item.item.renderedPeer.chatMainPeer?.compactDisplayTitle ?? " " + } + + if let avatarIconContent, let icon = self.icon { + let avatarIconComponent = EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: avatarIconContent, + isVisibleForAnimations: false, + action: nil + ) + let _ = icon.update( + transition: .immediate, + component: AnyComponent(avatarIconComponent), + environment: {}, + containerSize: iconSize + ) + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleText, font: Font.medium(14.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.secondaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 6.0 * 2.0, height: 100.0) + ) + + var badgeSize: CGSize? + if let readCounters = component.item.item.readCounters, readCounters.count > 0 { + let badge: ComponentView + var badgeTransition = transition + if let current = self.badge { + badge = current + } else { + badgeTransition = .immediate + badge = ComponentView() + self.badge = badge + } + + badgeSize = badge.update( + transition: badgeTransition, + component: AnyComponent(TextBadgeComponent( + text: countString(Int64(readCounters.count)), + font: Font.medium(12.0), + background: component.item.item.isMuted ? component.theme.chatList.unreadBadgeInactiveBackgroundColor : component.theme.chatList.unreadBadgeActiveBackgroundColor, + foreground: component.item.item.isMuted ? component.theme.chatList.unreadBadgeInactiveTextColor : component.theme.chatList.unreadBadgeActiveTextColor, + insets: UIEdgeInsets(top: 1.0, left: 5.0, bottom: 2.0, right: 5.0) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + } else if let badge = self.badge { + self.badge = nil + badge.view?.removeFromSuperview() + } + + var contentSize: CGFloat = leftInset + rightInset + iconSize.width + spacing + titleSize.width + if let badgeSize { + contentSize += badgeSize.width + badgeSpacing + } + let size = CGSize(width: contentSize, height: availableSize.height) + + let iconFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - iconSize.height) * 0.5)), size: iconSize) + let titleFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + spacing, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize) + + if let icon = self.icon { + if let avatarNode = self.avatarNode { + self.avatarNode = nil + avatarNode.view.removeFromSuperview() + } + + if let iconView = icon.view { + if iconView.superview == nil { + iconView.isUserInteractionEnabled = false + self.containerButton.addSubview(iconView) + } + iconView.frame = iconFrame + } + } else { + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0)) + avatarNode.isUserInteractionEnabled = false + self.avatarNode = avatarNode + self.containerButton.addSubview(avatarNode.view) + } + avatarNode.frame = iconFrame + avatarNode.updateSize(size: iconFrame.size) + + if let peer = component.item.item.renderedPeer.chatMainPeer { + if peer.smallProfileImage != nil { + avatarNode.setPeerV2(context: component.context, theme: component.theme, peer: peer, overrideImage: nil, emptyColor: .gray, clipStyle: .round, synchronousLoad: false, displayDimensions: iconFrame.size) + } else { + avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, overrideImage: nil, emptyColor: .gray, clipStyle: .round, synchronousLoad: false, displayDimensions: iconFrame.size) + } + } + } + + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.containerButton.addSubview(titleView) + } + titleView.frame = titleFrame + } + + if let badge = self.badge, let badgeSize { + let badgeFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + badgeSpacing, y: floor((size.height - badgeSize.height) * 0.5)), size: badgeSize) + if let badgeView = badge.view { + if badgeView.superview == nil { + self.containerButton.addSubview(badgeView) + } + badgeView.frame = badgeFrame + } + } + + transition.setFrame(view: self.containerButton, frame: CGRect(origin: CGPoint(), size: size)) + + self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size) + self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size) + self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) + self.containerNode.frame = CGRect(origin: CGPoint(), size: size) + + self.updateIsShaking(animated: !transition.animation.isImmediate) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } + } + + private final class TabItemView: UIView { + private let context: AccountContext + private let action: () -> Void + + private let extractedContainerNode: ContextExtractedContentContainingNode + private let containerNode: ContextControllerSourceNode + + private let containerButton: HighlightTrackingButton + + private var icon = ComponentView() + + private var isReordering: Bool = false + + init(context: AccountContext, action: @escaping (() -> Void)) { + self.context = context + self.action = action + + self.extractedContainerNode = ContextExtractedContentContainingNode() + self.containerNode = ContextControllerSourceNode() + + self.containerButton = HighlightTrackingButton() + + super.init(frame: CGRect()) + + self.extractedContainerNode.contentNode.view.addSubview(self.containerButton) + + self.containerNode.addSubnode(self.extractedContainerNode) + self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode + self.addSubview(self.containerNode.view) + + self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + self.containerButton.highligthedChanged = { [weak self] highlighted in + if let self, self.bounds.width > 0.0 { + let topScale: CGFloat = (self.bounds.width - 1.0) / self.bounds.width + let maxScale: CGFloat = (self.bounds.width + 1.0) / self.bounds.width + + if highlighted { + self.layer.removeAnimation(forKey: "opacity") + self.layer.removeAnimation(forKey: "sublayerTransform") + let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut) + transition.updateTransformScale(layer: self.layer, scale: topScale) + } else { + let transition: ContainedViewLayoutTransition = .immediate + transition.updateTransformScale(layer: self.layer, scale: 1.0) + + self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + + self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + }) + } + } + } + + self.containerNode.isGestureEnabled = false + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + @objc private func pressed() { + self.action() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + + func update(context: AccountContext, theme: PresentationTheme, width: CGFloat, location: Location, isReordering: Bool, transition: ComponentTransition) -> CGSize { + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2) + + var animateIconIn = false + if self.isReordering != isReordering { + self.isReordering = isReordering + if let iconView = self.icon.view { + self.icon = ComponentView() + transition.setScale(view: iconView, scale: 0.001) + alphaTransition.setAlpha(view: iconView, alpha: 0.0, completion: { [weak iconView] _ in + iconView?.removeFromSuperview() + }) + animateIconIn = true + } + } + + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: isReordering ? "Media Editor/Done" : "Chat/Title Panels/SidebarIcon", + tintColor: location == .side ? theme.rootController.navigationBar.accentTextColor : theme.rootController.navigationBar.secondaryTextColor, + maxSize: CGSize(width: 24.0, height: 24.0), + scaleFactor: 1.0 + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let topInset: CGFloat = 10.0 + let bottomInset: CGFloat = 2.0 + + let contentSize: CGFloat = topInset + iconSize.height + bottomInset + let size = CGSize(width: width, height: contentSize) + + let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) * 0.5), y: topInset), size: iconSize) + + if let iconView = self.icon.view { + if iconView.superview == nil { + iconView.isUserInteractionEnabled = false + self.containerButton.addSubview(iconView) + } + iconView.frame = iconFrame + if animateIconIn { + alphaTransition.animateAlpha(view: iconView, from: 0.0, to: 1.0) + transition.animateScale(view: iconView, from: 0.001, to: 1.0) + } + } + + transition.setFrame(view: self.containerButton, frame: CGRect(origin: CGPoint(), size: size)) + + self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size) + self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size) + self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) + self.containerNode.frame = CGRect(origin: CGPoint(), size: size) + + return size + } + } + + private protocol AllItemComponent: AnyObject { + } + + private final class VerticalAllItemComponent: Component, AllItemComponent { + let isSelected: Bool + let kind: ChatFloatingTopicsPanel.Kind + let theme: PresentationTheme + let strings: PresentationStrings + let action: (() -> Void)? + + init(isSelected: Bool, kind: ChatFloatingTopicsPanel.Kind, theme: PresentationTheme, strings: PresentationStrings, action: (() -> Void)?) { + self.isSelected = isSelected + self.kind = kind + self.theme = theme + self.strings = strings + self.action = action + } + + static func ==(lhs: VerticalAllItemComponent, rhs: VerticalAllItemComponent) -> Bool { + if lhs === rhs { + return true + } + if lhs.isSelected != rhs.isSelected { + return false + } + if lhs.kind != rhs.kind { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if (lhs.action == nil) != (rhs.action == nil) { + return false + } + return true + } + + final class View: UIView { + private let containerButton: UIView + + private let icon = ComponentView() + private let title = ComponentView() + + private var tapRecognizer: UITapGestureRecognizer? + + private var component: VerticalAllItemComponent? + + override init(frame: CGRect) { + self.containerButton = UIView() + + super.init(frame: frame) + + self.addSubview(self.containerButton) + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + self.tapRecognizer = tapRecognizer + self.containerButton.addGestureRecognizer(tapRecognizer) + tapRecognizer.isEnabled = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.component?.action?() + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + var mappedPoint = point + if self.bounds.insetBy(dx: -8.0, dy: -4.0).contains(point) { + mappedPoint = self.bounds.center + } + return super.hitTest(mappedPoint, with: event) + } + + func update(component: VerticalAllItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + self.tapRecognizer?.isEnabled = component.action != nil + + let topInset: CGFloat = 6.0 + let bottomInset: CGFloat = 8.0 + + let spacing: CGFloat = 1.0 + + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: "Chat List/Tabs/IconChats", + tintColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.secondaryTextColor + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let titleText: String + if case .botForum = component.kind { + //TODO:localize + titleText = "New Chat" + } else { + titleText = component.strings.Chat_InlineTopicMenu_AllTab + } + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleText, font: Font.regular(10.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.secondaryTextColor)), + maximumNumberOfLines: 2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 4.0 * 2.0, height: 100.0) + ) + + let contentSize: CGFloat = topInset + bottomInset + iconSize.height + spacing + titleSize.height + let size = CGSize(width: availableSize.width, height: contentSize) + + let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) * 0.5), y: topInset), size: iconSize) + let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: iconFrame.maxY + spacing), size: titleSize) + + if let iconView = self.icon.view { + if iconView.superview == nil { + iconView.isUserInteractionEnabled = false + self.containerButton.addSubview(iconView) + } + iconView.frame = iconFrame + } + + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.containerButton.addSubview(titleView) + } + titleView.frame = titleFrame + } + + transition.setFrame(view: self.containerButton, frame: CGRect(origin: CGPoint(), size: size)) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } + } + + private final class HorizontalAllItemComponent: Component, AllItemComponent { + let isSelected: Bool + let kind: ChatFloatingTopicsPanel.Kind + let theme: PresentationTheme + let strings: PresentationStrings + let action: (() -> Void)? + + init(isSelected: Bool, kind: ChatFloatingTopicsPanel.Kind, theme: PresentationTheme, strings: PresentationStrings, action: (() -> Void)?) { + self.isSelected = isSelected + self.kind = kind + self.theme = theme + self.strings = strings + self.action = action + } + + static func ==(lhs: HorizontalAllItemComponent, rhs: HorizontalAllItemComponent) -> Bool { + if lhs === rhs { + return true + } + if lhs.isSelected != rhs.isSelected { + return false + } + if lhs.kind != rhs.kind { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if (lhs.action == nil) != (rhs.action == nil) { + return false + } + return true + } + + final class View: UIView { + private let containerButton: UIView + + private let title = ComponentView() + + private var tapRecognizer: UITapGestureRecognizer? + + private var component: HorizontalAllItemComponent? + + override init(frame: CGRect) { + self.containerButton = UIView() + + super.init(frame: frame) + + self.addSubview(self.containerButton) + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + self.tapRecognizer = tapRecognizer + self.containerButton.addGestureRecognizer(tapRecognizer) + tapRecognizer.isEnabled = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.component?.action?() + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + var mappedPoint = point + if self.bounds.insetBy(dx: -8.0, dy: -4.0).contains(point) { + mappedPoint = self.bounds.center + } + return super.hitTest(mappedPoint, with: event) + } + + func update(component: HorizontalAllItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + self.tapRecognizer?.isEnabled = component.action != nil + + let leftInset: CGFloat = 6.0 + let rightInset: CGFloat = 12.0 + + let titleText: String + if case .botForum = component.kind { + //TODO:localize + titleText = "New Chat" + } else { + titleText = component.strings.Chat_InlineTopicMenu_AllTab + } + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleText, font: Font.medium(14.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.secondaryTextColor)), + maximumNumberOfLines: 2 + )), + environment: {}, + containerSize: CGSize(width: 400.0, height: 200.0) + ) + + let contentSize: CGFloat = leftInset + rightInset + titleSize.width + let size = CGSize(width: contentSize, height: availableSize.height) + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize) + + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.containerButton.addSubview(titleView) + } + titleView.frame = titleFrame + } + + transition.setFrame(view: self.containerButton, frame: CGRect(origin: CGPoint(), size: size)) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } + } + + private enum ScrollId: Hashable { + case all + case topic(Int64) + } + + public final class View: UIView { + private let list = ComponentView() + private let listState = AsyncListComponent.ExternalState() + private let scrollContainerView: UIView + private let scrollViewMask: UIImageView + + private var background: ComponentView? + private var separatorLayer: SimpleLayer? + + private let selectedLineContainer: AsyncListComponent.OverlayContainerView + private let selectedLineView: UIImageView + private let pinnedBackgroundContainer: AsyncListComponent.OverlayContainerView + private let pinnedBackgroundView: UIImageView + private let pinnedIconView: UIImageView + + private var tabItemView: TabItemView? + + private var peerId: EnginePeer.Id? + private var rawItems: [Item] = [] + private var reorderingItems: [Item]? + private var resetReorderingOnNextUpdate: Bool = false + private var itemsContentVersion: Int = 0 + + private var isTogglingPinnedItem: Bool = false + private weak var dismissContextControllerOnNextUpdate: ContextController? + + private var component: ChatFloatingTopicsPanel? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + + private var appliedScrollToId: ScrollId? + private var isReordering: Bool = false + + private var itemsDisposable: Disposable? + + override public init(frame: CGRect) { + self.selectedLineView = UIImageView() + self.selectedLineView.isHidden = true + self.selectedLineContainer = AsyncListComponent.OverlayContainerView() + self.selectedLineContainer.addSubview(self.selectedLineView) + + self.pinnedIconView = UIImageView() + self.pinnedBackgroundView = UIImageView() + self.pinnedBackgroundContainer = AsyncListComponent.OverlayContainerView() + self.pinnedBackgroundContainer.addSubview(self.pinnedIconView) + self.pinnedBackgroundContainer.addSubview(self.pinnedBackgroundView) + self.pinnedBackgroundContainer.isHidden = true + + self.scrollContainerView = UIView() + self.scrollViewMask = UIImageView() + self.scrollContainerView.mask = self.scrollViewMask + + super.init(frame: frame) + + self.addSubview(self.scrollContainerView) + self.scrollContainerView.addSubview(self.pinnedBackgroundContainer) + self.scrollContainerView.addSubview(self.selectedLineContainer) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.itemsDisposable?.dispose() + } + + public func updateGlobalOffset(globalOffset: CGFloat, transition: ComponentTransition) { + guard let component = self.component else { + return + } + if let tabItemView = self.tabItemView { + switch component.location { + case .side: + transition.setTransform(view: tabItemView, transform: CATransform3DMakeTranslation(-globalOffset, 0.0, 0.0)) + case .top: + transition.setTransform(view: tabItemView, transform: CATransform3DMakeTranslation(0.0, -globalOffset, 0.0)) + } + } + } + + public func topicIndex(threadId: Int64?) -> Int? { + if let threadId { + if let value = self.rawItems.firstIndex(where: { item in + if item.id == .chatList(PeerId(threadId)) { + return true + } else if item.id == .forum(threadId) { + return true + } else { + return false + } + }) { + return value + 1 + } else { + return nil + } + } else { + return 0 + } + } + + private func updateListOverlays(visibleItems: AsyncListComponent.VisibleItems, transition: ComponentTransition) { + guard let component = self.component, let listView = self.list.view else { + return + } + + var selectedItemFrame: CGRect? + var beforePinnedItemsPosition: CGFloat? + var afterPinnedItemsPosition: CGFloat? + var seenPinnedItems = false + for item in visibleItems { + if let _ = item.item.component.wrapped as? AllItemComponent { + if component.topicId == nil { + switch component.location { + case .side: + selectedItemFrame = item.frame + case .top: + selectedItemFrame = CGRect(origin: CGPoint(x: item.frame.minX + 5.0, y: item.frame.minY), size: CGSize(width: item.frame.width - 4.0 - 11.0, height: item.frame.height)) + } + } + if !seenPinnedItems { + switch component.location { + case .side: + beforePinnedItemsPosition = item.frame.maxY + case .top: + beforePinnedItemsPosition = item.frame.maxX + } + } + } else if let itemComponent = item.item.component.wrapped as? ItemComponent { + let topicId: Int64 + switch itemComponent.item.item.id { + case let .chatList(peerId): + topicId = peerId.toInt64() + case let .forum(topicIdValue): + topicId = topicIdValue + } + if topicId == component.topicId { + selectedItemFrame = item.frame + } + + var isPinned = false + if case let .forum(pinnedIndex, _, _, _, _) = itemComponent.item.item.index { + if case .index = pinnedIndex { + isPinned = true + } + } + if isPinned { + seenPinnedItems = true + } else { + if !seenPinnedItems { + switch component.location { + case .side: + beforePinnedItemsPosition = item.frame.maxY + case .top: + beforePinnedItemsPosition = item.frame.maxX + } + } else { + if afterPinnedItemsPosition == nil { + switch component.location { + case .side: + afterPinnedItemsPosition = item.frame.minY + case .top: + afterPinnedItemsPosition = item.frame.minX + } + } + } + } + } + } + + if seenPinnedItems { + if beforePinnedItemsPosition == nil { + beforePinnedItemsPosition = -500.0 + } + if afterPinnedItemsPosition == nil { + switch component.location { + case .side: + afterPinnedItemsPosition = listView.bounds.height + 500.0 + case .top: + afterPinnedItemsPosition = listView.bounds.width + 500.0 + } + } + } + + if let selectedItemFrame { + var lineTransition = transition + if self.selectedLineView.isHidden { + self.selectedLineView.isHidden = false + lineTransition = .immediate + } + let selectedLineFrame: CGRect + switch component.location { + case .side: + selectedLineFrame = CGRect(origin: CGPoint(x: 0.0, y: selectedItemFrame.minY), size: CGSize(width: 4.0, height: selectedItemFrame.height)) + case .top: + selectedLineFrame = CGRect(origin: CGPoint(x: selectedItemFrame.minX, y: listView.frame.maxY - 3.0), size: CGSize(width: selectedItemFrame.width, height: 3.0)) + } + + self.selectedLineContainer.updatePosition(position: selectedLineFrame.origin, transition: lineTransition) + lineTransition.setFrame(view: self.selectedLineView, frame: CGRect(origin: CGPoint(), size: selectedLineFrame.size)) + } else { + self.selectedLineView.isHidden = true + } + + if let beforePinnedItemsPosition, let afterPinnedItemsPosition, afterPinnedItemsPosition > beforePinnedItemsPosition { + var pinnedItemsTransition = transition + if self.pinnedBackgroundContainer.isHidden { + self.pinnedBackgroundContainer.isHidden = false + pinnedItemsTransition = .immediate + } + let pinnedItemsBackgroundFrame: CGRect + switch component.location { + case .side: + pinnedItemsBackgroundFrame = CGRect(origin: CGPoint(x: 5.0, y: beforePinnedItemsPosition), size: CGSize(width: listView.bounds.width - 5.0 - 4.0, height: afterPinnedItemsPosition - beforePinnedItemsPosition)) + case .top: + pinnedItemsBackgroundFrame = CGRect(origin: CGPoint(x: beforePinnedItemsPosition, y: 4.0), size: CGSize(width: afterPinnedItemsPosition - beforePinnedItemsPosition, height: listView.bounds.height - 5.0 - 4.0)) + } + self.pinnedBackgroundContainer.updatePosition(position: pinnedItemsBackgroundFrame.origin, transition: pinnedItemsTransition) + pinnedItemsTransition.setFrame(view: self.pinnedBackgroundView, frame: CGRect(origin: CGPoint(), size: pinnedItemsBackgroundFrame.size)) + + let pinnedIconFrame = CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: 12.0, height: 12.0)) + pinnedItemsTransition.setFrame(view: self.pinnedIconView, frame: pinnedIconFrame) + } else { + self.pinnedBackgroundContainer.isHidden = true + } + } + + private func updateIsReordering(isReordering: Bool) { + self.isReordering = isReordering + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + } + + func update(component: ChatFloatingTopicsPanel, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + self.state = state + + if self.resetReorderingOnNextUpdate { + self.resetReorderingOnNextUpdate = false + self.reorderingItems = nil + self.isReordering = false + } + + if self.component == nil { + let threadListSignal: Signal<(EnginePeer.Id, EngineChatList), NoError> + + switch component.kind { + default: + let defaultPeerId = component.peerId + threadListSignal = component.context.sharedContext.subscribeChatListData(context: component.context, location: component.kind == .monoforum ? .savedMessagesChats(peerId: component.peerId) : .forum(peerId: component.peerId)) + |> map { value in + return (defaultPeerId, value) + } + } + + self.itemsDisposable = (threadListSignal + |> deliverOnMainQueue).startStrict(next: { [weak self] peerId, chatList in + guard let self, let _ = self.component else { + return + } + + self.peerId = peerId + + let wasEmpty = self.rawItems.isEmpty + + self.rawItems.removeAll() + for item in chatList.items.reversed() { + self.rawItems.append(Item(item: item)) + } + + if self.reorderingItems != nil { + self.reorderingItems = self.rawItems + } + + if !self.isUpdating { + self.state?.updated(transition: (wasEmpty || self.isTogglingPinnedItem) ? .immediate : .spring(duration: 0.4)) + } + }) + + switch component.location { + case .side: + self.scrollViewMask.image = generateGradientImage(size: CGSize(width: 8.0, height: 8.0), colors: [ + UIColor(white: 1.0, alpha: 0.0), + UIColor(white: 1.0, alpha: 1.0) + ], locations: [0.0, 1.0], direction: .vertical)?.stretchableImage(withLeftCapWidth: 0, topCapHeight: 8) + case .top: + self.scrollViewMask.image = generateGradientImage(size: CGSize(width: 8.0, height: 8.0), colors: [ + UIColor(white: 1.0, alpha: 0.0), + UIColor(white: 1.0, alpha: 1.0) + ], locations: [0.0, 1.0], direction: .horizontal)?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 0) + } + } + let themeUpdated = self.component?.theme !== component.theme + self.component = component + + if case .side = component.location { + let background: ComponentView + if let current = self.background { + background = current + } else { + background = ComponentView() + self.background = background + } + let _ = background.update( + transition: transition, + component: AnyComponent(BlurredBackgroundComponent( + color: component.theme.rootController.navigationBar.blurredBackgroundColor + )), + environment: {}, + containerSize: availableSize + ) + + if let backgroundView = background.view { + if backgroundView.superview == nil { + self.insertSubview(backgroundView, at: 0) + } + transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: availableSize)) + } + + let separatorLayer: SimpleLayer + if let current = self.separatorLayer { + separatorLayer = current + } else { + separatorLayer = SimpleLayer() + self.separatorLayer = separatorLayer + self.layer.addSublayer(separatorLayer) + } + if themeUpdated { + separatorLayer.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor + } + + transition.setFrame(layer: separatorLayer, frame: CGRect(origin: CGPoint(x: availableSize.width, y: 0.0), size: CGSize(width: UIScreenPixel, height: availableSize.height))) + } + + if themeUpdated { + switch component.location { + case .side: + self.selectedLineView.image = generateImage(CGSize(width: 4.0, height: 7.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(component.theme.rootController.navigationBar.accentTextColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height, y: 0.0), size: CGSize(width: size.height, height: size.height))) + })?.stretchableImage(withLeftCapWidth: 1, topCapHeight: 4) + case .top: + self.selectedLineView.image = generateImage(CGSize(width: 4.0, height: 3.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(component.theme.rootController.navigationBar.accentTextColor.cgColor) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height * 2.0)), cornerRadius: 2.0).cgPath) + context.fillPath() + })?.stretchableImage(withLeftCapWidth: 2, topCapHeight: 1) + } + + if self.pinnedIconView.image == nil { + self.pinnedIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/Pinned"), color: .white)?.withRenderingMode(.alwaysTemplate) + } + self.pinnedIconView.tintColor = component.theme.chatList.unreadBadgeInactiveBackgroundColor + + if self.pinnedBackgroundView.image == nil { + self.pinnedBackgroundView.image = generateStretchableFilledCircleImage(diameter: 10.0, color: .white)?.withRenderingMode(.alwaysTemplate) + } + var pinnedBackgroundColor = component.theme.rootController.navigationSearchBar.inputFillColor + if pinnedBackgroundColor.distance(to: component.theme.list.blocksBackgroundColor) < 100 { + pinnedBackgroundColor = pinnedBackgroundColor.withMultipliedBrightnessBy(0.8) + } + self.pinnedBackgroundView.tintColor = pinnedBackgroundColor + } + + let environment = environment[EnvironmentType.self].value + + let containerInsets = environment.insets + + var directionContainerInset: CGFloat + switch component.location { + case .side: + directionContainerInset = containerInsets.top + case .top: + directionContainerInset = containerInsets.left + } + + do { + var itemTransition = transition + var animateIn = false + let itemView: TabItemView + if let current = self.tabItemView { + itemView = current + } else { + itemTransition = .immediate + animateIn = true + itemView = TabItemView(context: component.context, action: { [weak self] in + guard let self, let peerId = self.peerId, let component = self.component else { + return + } + if self.isReordering { + if let reorderingItems = self.reorderingItems { + var threadIds: [Int64] = [] + for item in reorderingItems { + if case let .forum(pinnedIndex, _, threadId, _, _) = item.item.index, case .index = pinnedIndex { + threadIds.append(threadId) + } + } + + var currentThreadIds: [Int64] = [] + for item in self.rawItems { + if case let .forum(pinnedIndex, _, threadId, _, _) = item.item.index, case .index = pinnedIndex { + currentThreadIds.append(threadId) + } + } + + if threadIds != currentThreadIds { + let _ = component.context.engine.peers.setForumChannelPinnedTopics(id: peerId, threadIds: threadIds).startStandalone() + self.resetReorderingOnNextUpdate = true + } else { + self.reorderingItems = nil + self.isReordering = false + self.state?.updated(transition: .spring(duration: 0.4)) + } + } else { + self.isReordering = false + self.state?.updated(transition: .spring(duration: 0.4)) + } + } else { + component.togglePanel() + } + }) + self.tabItemView = itemView + self.addSubview(itemView) + } + + let itemSize = itemView.update(context: component.context, theme: component.theme, width: 72.0, location: component.location, isReordering: self.isReordering, transition: itemTransition) + let itemFrame: CGRect + switch component.location { + case .side: + itemFrame = CGRect(origin: CGPoint(x: 0.0, y: directionContainerInset), size: itemSize) + directionContainerInset += itemSize.height + case .top: + itemFrame = CGRect(origin: CGPoint(x: directionContainerInset, y: 0.0), size: itemSize) + directionContainerInset += itemSize.width - 14.0 + } + + itemTransition.setPosition(layer: itemView.layer, position: itemFrame.center) + itemTransition.setBounds(layer: itemView.layer, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) + + if animateIn && !transition.animation.isImmediate { + itemView.layer.animateAlpha(from: 0.0, to: itemView.alpha, duration: 0.15) + transition.containedViewLayoutTransition.animateTransformScale(view: itemView, from: 0.001) + } + } + + let scrollSize: CGSize + let scrollFrame: CGRect + let listContentInsets: UIEdgeInsets + switch component.location { + case .side: + scrollSize = CGSize(width: availableSize.width, height: availableSize.height - directionContainerInset) + scrollFrame = CGRect(origin: CGPoint(x: 0.0, y: directionContainerInset), size: scrollSize) + listContentInsets = UIEdgeInsets(top: 8.0 + environment.insets.top, left: 0.0, bottom: 8.0 + environment.insets.bottom, right: 0.0) + case .top: + scrollSize = CGSize(width: availableSize.width - directionContainerInset, height: availableSize.height) + scrollFrame = CGRect(origin: CGPoint(x: directionContainerInset, y: 0.0), size: scrollSize) + listContentInsets = UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 8.0) + } + + self.scrollContainerView.frame = scrollFrame + self.scrollViewMask.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: scrollSize) + + let scrollToId: ScrollId + if let threadId = component.topicId { + scrollToId = .topic(threadId) + } else { + scrollToId = .all + } + if self.appliedScrollToId != scrollToId { + self.appliedScrollToId = scrollToId + self.listState.resetScrolling(id: AnyHashable(scrollToId)) + } + + var listItems: [AnyComponentWithIdentity] = [] + switch component.location { + case .side: + listItems.append(AnyComponentWithIdentity( + id: ScrollId.all, + component: AnyComponent(VerticalAllItemComponent( + isSelected: component.topicId == nil, + kind: component.kind, + theme: component.theme, + strings: component.strings, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.updateTopicId(nil, false) + } + ))) + ) + case .top: + listItems.append(AnyComponentWithIdentity( + id: ScrollId.all, + component: AnyComponent(HorizontalAllItemComponent( + isSelected: component.topicId == nil, + kind: component.kind, + theme: component.theme, + strings: component.strings, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.updateTopicId(nil, false) + } + ))) + ) + } + for item in self.reorderingItems ?? self.rawItems { + let scrollId: ScrollId + let topicId: Int64 + var isItemReordering = false + switch item.item.id { + case let .chatList(peerId): + topicId = peerId.toInt64() + case let .forum(topicIdValue): + topicId = topicIdValue + if self.isReordering { + if case let .forum(pinnedIndex, _, _, _, _) = item.item.index, case .index = pinnedIndex { + isItemReordering = true + } + } + } + scrollId = .topic(topicId) + + let itemAction: (() -> Void)? = self.isReordering ? nil : { [weak self] in + guard let self, let component = self.component else { + return + } + + let direction: Bool + if let lhsIndex = self.topicIndex(threadId: component.topicId), let rhsIndex = self.topicIndex(threadId: topicId) { + direction = lhsIndex < rhsIndex + } else { + direction = false + } + component.updateTopicId(topicId, direction) + } + var itemContextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)? + if !self.isReordering, case .monoforum = component.kind { + itemContextGesture = { [weak self] gesture, sourceNode in + Task { @MainActor in + guard let self, let peerId = self.peerId, let component = self.component else { + return + } + guard let controller = component.controller() else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + + if let listView = self.list.view as? AsyncListComponent.View { + listView.stopScrolling() + } + + let topicId: Int64 + switch item.item.id { + case let .chatList(peerId): + topicId = peerId.toInt64() + case let .forum(topicIdValue): + topicId = topicIdValue + } + + var items: [ContextMenuItem] = [] + + let threadInfo = await component.context.engine.data.get( + TelegramEngine.EngineData.Item.Messages.ThreadInfo(peerId: peerId, threadId: topicId) + ).get() + + if let threadInfo, threadInfo.isMessageFeeRemoved { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ReinstatePaidMessages, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + guard let self, let peerId = self.peerId, let component = self.component else { + return + } + + c?.dismiss(completion: {}) + + let _ = component.context.engine.peers.reinstateNoPaidMessagesException(scopePeerId: peerId, peerId: EnginePeer.Id(topicId)).startStandalone() + }))) + } + + if !items.isEmpty { + items.append(.separator) + } + items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, _ in + guard let self else { + return + } + + c?.dismiss(completion: { [weak self] in + guard let self, let component = self.component else { + return + } + component.openDeletePeer(topicId) + }) + }))) + + let contextController = ContextController( + presentationData: presentationData, + source: .extracted(ItemExtractedContentSource( + sourceNode: sourceNode, + containerView: self, + keepInPlace: false + )), + items: .single(ContextController.Items(content: .list(items))), + recognizer: nil, + gesture: gesture + ) + controller.presentInGlobalOverlay(contextController) + } + } + } else if !self.isReordering { + itemContextGesture = { [weak self] gesture, sourceNode in + guard let self, let peerId = self.peerId, let component = self.component else { + return + } + guard let controller = component.controller() else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + + if let listView = self.list.view as? AsyncListComponent.View { + listView.stopScrolling() + } + + let topicId: Int64 + switch item.item.id { + case let .chatList(peerId): + topicId = peerId.toInt64() + case let .forum(topicIdValue): + topicId = topicIdValue + } + + var isPinned = false + if case let .forum(pinnedIndex, _, _, _, _) = item.item.index { + if case .index = pinnedIndex { + isPinned = true + } + } + let isClosed = item.item.threadData?.isClosed + let threadData = item.item.threadData + + let _ = (chatForumTopicMenuItems( + context: component.context, + peerId: peerId, + threadId: topicId, + isPinned: isPinned, + isClosed: isClosed, + chatListController: controller, + joined: true, + canSelect: false, + customEdit: { [weak self] contextController in + contextController.dismiss(completion: { + guard let self, let peerId = self.peerId, let component = self.component, let threadData else { + return + } + let editController = component.context.sharedContext.makeEditForumTopicScreen( + context: component.context, + peerId: peerId, + threadId: topicId, + threadInfo: threadData.info, + isHidden: threadData.isHidden + ) + component.controller()?.push(editController) + }) + }, + customPinUnpin: { [weak self] contextController in + guard let self, let peerId = self.peerId, let component = self.component else { + contextController.dismiss(completion: {}) + return + } + + self.isTogglingPinnedItem = true + self.dismissContextControllerOnNextUpdate = contextController + + let _ = (component.context.engine.peers.toggleForumChannelTopicPinned(id: peerId, threadId: topicId) + |> deliverOnMainQueue).startStandalone(error: { [weak self, weak contextController] error in + guard let self, let component = self.component else { + contextController?.dismiss(completion: {}) + return + } + + switch error { + case let .limitReached(count): + contextController?.dismiss(completion: {}) + if let controller = component.controller() { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let text = presentationData.strings.ChatList_MaxThreadPinsFinalText(Int32(count)) + controller.present(textAlertController(context: component.context, title: presentationData.strings.Premium_LimitReached, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], parseMarkdown: true), in: .window(.root)) + } + default: + break + } + }) + }, + reorder: { [weak self] in + guard let self else { + return + } + self.updateIsReordering(isReordering: true) + }, + onDeleted: { [weak self] in + guard let self, let component = self.component else { + return + } + component.updateTopicId(nil, false) + } + ) + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak sourceNode, weak gesture] items in + guard let self, let component = self.component else { + return + } + guard let controller = component.controller() else { + return + } + guard let sourceNode else { + return + } + + let contextController = ContextController( + presentationData: presentationData, + source: .extracted(ItemExtractedContentSource( + sourceNode: sourceNode, + containerView: self, + keepInPlace: false + )), + items: .single(ContextController.Items(content: .list(items))), + recognizer: nil, + gesture: gesture + ) + controller.presentInGlobalOverlay(contextController) + }) + } + } + + switch component.location { + case .side: + listItems.append(AnyComponentWithIdentity( + id: scrollId, + component: AnyComponent(VerticalItemComponent( + context: component.context, + item: item, + isSelected: component.topicId == topicId, + isReordering: isItemReordering, + theme: component.theme, + strings: component.strings, + action: itemAction, + contextGesture: itemContextGesture + ))) + ) + case .top: + listItems.append(AnyComponentWithIdentity( + id: scrollId, + component: AnyComponent(HorizontalItemComponent( + context: component.context, + item: item, + isSelected: component.topicId == topicId, + isReordering: isItemReordering, + theme: component.theme, + strings: component.strings, + action: itemAction, + contextGesture: itemContextGesture + ))) + ) + } + } + + let _ = self.list.update( + transition: transition, + component: AnyComponent(AsyncListComponent( + externalState: self.listState, + items: listItems, + itemSetId: AnyHashable(self.itemsContentVersion), + direction: component.location == .side ? .vertical : .horizontal, + insets: listContentInsets, + reorderItems: { [weak self] fromIndex, toIndex in + guard let self else { + return false + } + if !self.isReordering { + return false + } + + if self.reorderingItems == nil { + self.reorderingItems = self.rawItems + } + if var reorderingItems = self.reorderingItems { + var maxToIndex = -1 + for item in reorderingItems { + if case let .forum(pinnedIndex, _, _, _, _) = item.item.index, case .index = pinnedIndex { + maxToIndex += 1 + } else { + break + } + } + + let fromItemIndex = fromIndex - 1 + // Account for synthesized "all" item: [all, item_0, item_1, ...] + let toItemIndex = max(0, min(maxToIndex, toIndex - 1)) + if fromItemIndex == toItemIndex { + return false + } + + let reorderingItem = reorderingItems[fromItemIndex] + if toItemIndex < fromItemIndex { + reorderingItems.remove(at: fromItemIndex) + reorderingItems.insert(reorderingItem, at: toItemIndex) + } else { + reorderingItems.insert(reorderingItem, at: toItemIndex + 1) + reorderingItems.remove(at: fromItemIndex) + } + + self.reorderingItems = reorderingItems + self.state?.updated(transition: .spring(duration: 0.4)) + } + + return true + }, + onVisibleItemsUpdated: { [weak self] visibleItems, transition in + guard let self else { + return + } + self.updateListOverlays(visibleItems: visibleItems, transition: transition) + } + )), + environment: {}, + containerSize: scrollSize + ) + if let listView = self.list.view { + if listView.superview == nil { + self.scrollContainerView.addSubview(listView) + } + transition.setFrame(view: listView, frame: CGRect(origin: CGPoint(), size: scrollSize)) + } + + if self.isTogglingPinnedItem { + self.isTogglingPinnedItem = false + } + if let dismissContextControllerOnNextUpdate = self.dismissContextControllerOnNextUpdate { + self.dismissContextControllerOnNextUpdate = nil + dismissContextControllerOnNextUpdate.dismiss(completion: {}) + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class ItemExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool + let ignoreContentTouches: Bool = true + let blurBackground: Bool = true + let adjustContentForSideInset: Bool = true + + private let sourceNode: ContextExtractedContentContainingNode + private weak var containerView: UIView? + + init(sourceNode: ContextExtractedContentContainingNode, containerView: UIView, keepInPlace: Bool) { + self.sourceNode = sourceNode + self.containerView = containerView + self.keepInPlace = keepInPlace + } + + func takeView() -> ContextControllerTakeViewInfo? { + var contentArea: CGRect? + if let containerView = self.containerView { + contentArea = containerView.convert(containerView.bounds, to: nil) + } + + return ContextControllerTakeViewInfo( + containingItem: .node(self.sourceNode), + contentAreaInScreenSpace: contentArea ?? UIScreen.main.bounds + ) + } + + func putBack() -> ContextControllerPutBackViewInfo? { + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + } +}*/ diff --git a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift index 347736e57b..e8ef797c5a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift @@ -21,6 +21,7 @@ import TextBadgeComponent import MaskedContainerComponent import AppBundle import PresentationDataUtils +import GlassBackgroundComponent public final class ChatSidePanelEnvironment: Equatable { public let insets: UIEdgeInsets @@ -1442,7 +1443,6 @@ public final class ChatSideTopicsPanel: Component { private let scrollViewMask: UIImageView private var background: ComponentView? - private var separatorLayer: SimpleLayer? private let selectedLineContainer: AsyncListComponent.OverlayContainerView private let selectedLineView: UIImageView @@ -1486,6 +1486,7 @@ public final class ChatSideTopicsPanel: Component { self.scrollContainerView = UIView() self.scrollViewMask = UIImageView() self.scrollContainerView.mask = self.scrollViewMask + //self.scrollContainerView.addSubview(self.scrollViewMask) super.init(frame: frame) @@ -1721,10 +1722,72 @@ public final class ChatSideTopicsPanel: Component { switch component.location { case .side: - self.scrollViewMask.image = generateGradientImage(size: CGSize(width: 8.0, height: 8.0), colors: [ - UIColor(white: 1.0, alpha: 0.0), - UIColor(white: 1.0, alpha: 1.0) - ], locations: [0.0, 1.0], direction: .vertical)?.stretchableImage(withLeftCapWidth: 0, topCapHeight: 8) + let cornerRadius: CGFloat = 20.0 + self.scrollViewMask.image = generateImage(CGSize(width: 1.0 + cornerRadius * 2.0, height: 8.0 + 1.0 + cornerRadius * 2.0), rotatedContext: { size, context in + UIGraphicsPushContext(context) + defer { + UIGraphicsPopContext() + } + + context.clear(CGRect(origin: CGPoint(), size: size)) + + let spreadPath = UIBezierPath( + roundedRect: CGRect(origin: CGPoint(x: 0.0, y: -cornerRadius), size: CGSize(width: size.width, height: size.height + cornerRadius)), + cornerRadius: cornerRadius + ).cgPath + context.setFillColor(UIColor.black.cgColor) + context.addPath(spreadPath) + context.fillPath() + + if let image = generateGradientImage(size: CGSize(width: 8.0, height: 8.0), colors: [ + UIColor(white: 1.0, alpha: 1.0), + UIColor(white: 1.0, alpha: 0.0) + ], locations: [0.0, 1.0], direction: .vertical) { + image.draw(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: image.size.width)), blendMode: .destinationOut, alpha: 1.0) + } + + /*let innerSize = size + let shadowInset: CGFloat = 32.0 + + let addInnerShadow: (CGPoint, CGFloat, UIColor) -> Void = { position, blur, shadowColor in + if let image = generateImage(CGSize(width: size.width + shadowInset * 2.0, height: size.height + shadowInset * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + let spreadRect = CGRect(origin: CGPoint(x: shadowInset, y: shadowInset), size: innerSize).insetBy(dx: -0.25, dy: -0.25) + let spreadPath = UIBezierPath( + roundedRect: spreadRect, + cornerRadius: min(spreadRect.width, spreadRect.height) * 0.5 + ).cgPath + + context.setShadow(offset: CGSize(width: position.x, height: position.y), blur: blur, color: shadowColor.cgColor) + context.setFillColor(shadowColor.cgColor) + let enclosingRect = spreadRect.insetBy(dx: -10000.0, dy: -10000.0) + for _ in 0 ..< 3 { + context.addPath(UIBezierPath(rect: enclosingRect).cgPath) + context.addPath(spreadPath) + context.fillPath(using: .evenOdd) + } + + /*let cleanRect = CGRect(origin: CGPoint(x: shadowInset, y: shadowInset), size: innerSize) + let cleanPath = UIBezierPath( + roundedRect: cleanRect, + cornerRadius: min(cleanRect.width, cleanRect.height) * 0.5 + ).cgPath + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.addPath(UIBezierPath(rect: enclosingRect).cgPath) + context.addPath(cleanPath) + context.fillPath(using: .evenOdd) + context.setBlendMode(.normal)*/ + }) { + image.draw(in: CGRect(origin: CGPoint(x: -shadowInset, y: -shadowInset), size: CGSize(width: image.size.width, height: image.size.height)), blendMode: .normal, alpha: 1.0) + } + } + + addInnerShadow(CGPoint(x: 0.0, y: -6.0), 10.0, .red) + addInnerShadow(CGPoint(x: 0.0, y: 6.0), 10.0, .red)*/ + + + })?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 1, topCapHeight: Int(cornerRadius) + 1) case .top: self.scrollViewMask.image = generateGradientImage(size: CGSize(width: 8.0, height: 8.0), colors: [ UIColor(white: 1.0, alpha: 0.0), @@ -1735,45 +1798,6 @@ public final class ChatSideTopicsPanel: Component { let themeUpdated = self.component?.theme !== component.theme self.component = component - if case .side = component.location { - let background: ComponentView - if let current = self.background { - background = current - } else { - background = ComponentView() - self.background = background - } - let _ = background.update( - transition: transition, - component: AnyComponent(BlurredBackgroundComponent( - color: component.theme.rootController.navigationBar.blurredBackgroundColor - )), - environment: {}, - containerSize: availableSize - ) - - if let backgroundView = background.view { - if backgroundView.superview == nil { - self.insertSubview(backgroundView, at: 0) - } - transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: availableSize)) - } - - let separatorLayer: SimpleLayer - if let current = self.separatorLayer { - separatorLayer = current - } else { - separatorLayer = SimpleLayer() - self.separatorLayer = separatorLayer - self.layer.addSublayer(separatorLayer) - } - if themeUpdated { - separatorLayer.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor - } - - transition.setFrame(layer: separatorLayer, frame: CGRect(origin: CGPoint(x: availableSize.width, y: 0.0), size: CGSize(width: UIScreenPixel, height: availableSize.height))) - } - if themeUpdated { switch component.location { case .side: @@ -1871,7 +1895,7 @@ public final class ChatSideTopicsPanel: Component { let itemFrame: CGRect switch component.location { case .side: - itemFrame = CGRect(origin: CGPoint(x: 0.0, y: directionContainerInset), size: itemSize) + itemFrame = CGRect(origin: CGPoint(x: 8.0 + 4.0, y: directionContainerInset + 8.0), size: itemSize) directionContainerInset += itemSize.height case .top: itemFrame = CGRect(origin: CGPoint(x: directionContainerInset, y: 0.0), size: itemSize) @@ -1890,18 +1914,51 @@ public final class ChatSideTopicsPanel: Component { let scrollSize: CGSize let scrollFrame: CGRect let listContentInsets: UIEdgeInsets + let additionalInsets: UIEdgeInsets switch component.location { case .side: - scrollSize = CGSize(width: availableSize.width, height: availableSize.height - directionContainerInset) - scrollFrame = CGRect(origin: CGPoint(x: 0.0, y: directionContainerInset), size: scrollSize) - listContentInsets = UIEdgeInsets(top: 8.0 + environment.insets.top, left: 0.0, bottom: 8.0 + environment.insets.bottom, right: 0.0) + additionalInsets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 0.0) + scrollSize = CGSize(width: availableSize.width, height: availableSize.height - directionContainerInset - environment.insets.top - containerInsets.bottom - additionalInsets.top - additionalInsets.bottom) + scrollFrame = CGRect(origin: CGPoint(x: additionalInsets.left, y: directionContainerInset + environment.insets.top + additionalInsets.top), size: scrollSize) + listContentInsets = UIEdgeInsets(top: 8.0, left: 0.0, bottom: 8.0, right: 0.0) case .top: scrollSize = CGSize(width: availableSize.width - directionContainerInset, height: availableSize.height) scrollFrame = CGRect(origin: CGPoint(x: directionContainerInset, y: 0.0), size: scrollSize) listContentInsets = UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 8.0) + additionalInsets = UIEdgeInsets() + } + + if case .side = component.location { + let background: ComponentView + if let current = self.background { + background = current + } else { + background = ComponentView() + self.background = background + } + let backgroundFrame = CGRect(origin: CGPoint(x: scrollFrame.minX, y: environment.insets.top + additionalInsets.top), size: CGSize(width: scrollFrame.width, height: scrollFrame.height + directionContainerInset)) + let _ = background.update( + transition: transition, + component: AnyComponent(GlassBackgroundComponent( + size: backgroundFrame.size, + cornerRadius: 20.0, + isDark: component.theme.overallDarkAppearance, + tintColor: .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)) + )), + environment: {}, + containerSize: backgroundFrame.size + ) + + if let backgroundView = background.view { + if backgroundView.superview == nil { + self.insertSubview(backgroundView, at: 0) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } } self.scrollContainerView.frame = scrollFrame + self.scrollViewMask.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: scrollSize) let scrollToId: ScrollId diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift index d54bbcfe41..9ea11e21ff 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift @@ -196,8 +196,8 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag self.expandMediaInputButtonIcon = GlassBackgroundView.ContentImageView() self.expandMediaInputButtonBackgroundView.contentView.addSubview(self.expandMediaInputButtonIcon) self.expandMediaInputButtonIcon.image = PresentationResourcesChat.chatInputPanelExpandButtonImage(presentationInterfaceState.theme) - self.expandMediaInputButtonIcon.tintColor = theme.chat.inputPanel.inputControlColor - self.expandMediaInputButtonIcon.setMonochromaticEffect(tintColor: theme.chat.inputPanel.inputControlColor) + self.expandMediaInputButtonIcon.tintColor = theme.chat.inputPanel.panelControlColor + self.expandMediaInputButtonIcon.setMonochromaticEffect(tintColor: theme.chat.inputPanel.panelControlColor) super.init() @@ -265,8 +265,8 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag public func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) { self.micButton.updateTheme(theme: theme) - self.expandMediaInputButtonIcon.tintColor = theme.chat.inputPanel.inputControlColor - self.expandMediaInputButtonIcon.setMonochromaticEffect(tintColor: theme.chat.inputPanel.inputControlColor) + self.expandMediaInputButtonIcon.tintColor = theme.chat.inputPanel.panelControlColor + self.expandMediaInputButtonIcon.setMonochromaticEffect(tintColor: theme.chat.inputPanel.panelControlColor) } private var absoluteRect: (CGRect, CGSize)? diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingCancelIndicator/Sources/ChatTextInputAudioRecordingCancelIndicator.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingCancelIndicator/Sources/ChatTextInputAudioRecordingCancelIndicator.swift index 1535781c7b..18f1e3b354 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingCancelIndicator/Sources/ChatTextInputAudioRecordingCancelIndicator.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingCancelIndicator/Sources/ChatTextInputAudioRecordingCancelIndicator.swift @@ -27,7 +27,7 @@ public final class ChatTextInputAudioRecordingCancelIndicator: UIView, GlassBack self.arrowView = GlassBackgroundView.ContentImageView() self.arrowView.image = UIImage(bundleImageName: "Chat/Input/Text/AudioRecordingCancelArrow")?.withRenderingMode(.alwaysTemplate) - self.arrowView.tintColor = theme.chat.inputPanel.inputControlColor + self.arrowView.tintColor = theme.chat.inputPanel.panelControlColor self.labelNode = TextNode() self.labelNode.displaysAsynchronously = false @@ -56,7 +56,7 @@ public final class ChatTextInputAudioRecordingCancelIndicator: UIView, GlassBack let makeLayout = TextNode.asyncLayout(self.labelNode) let makeTintLayout = TextNode.asyncLayout(self.tintLabelNode) - let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: theme.chat.inputPanel.inputControlColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: theme.chat.inputPanel.panelControlColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (_, tintLabelApply) = makeTintLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: .black), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let _ = labelApply() let _ = tintLabelApply @@ -79,7 +79,7 @@ public final class ChatTextInputAudioRecordingCancelIndicator: UIView, GlassBack } public func updateTheme(theme: PresentationTheme) { - self.arrowView.tintColor = theme.chat.inputPanel.inputControlColor + self.arrowView.tintColor = theme.chat.inputPanel.panelControlColor self.cancelButton.setTitle(self.strings.Common_Cancel, with: cancelFont, with: theme.chat.inputPanel.panelControlAccentColor, for: []) let makeLayout = TextNode.asyncLayout(self.labelNode) let makeTintLayout = TextNode.asyncLayout(self.tintLabelNode) diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/AccessoryItemIconButton.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/AccessoryItemIconButton.swift index 74bb336023..006482b186 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/AccessoryItemIconButton.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/AccessoryItemIconButton.swift @@ -87,8 +87,13 @@ final class AccessoryItemIconButton: HighlightTrackingButton, GlassBackgroundVie } self.iconImageView.image = image - self.iconImageView.tintColor = theme.chat.inputPanel.inputControlColor - self.iconImageView.alpha = alpha + if #available(iOS 26.0, *) { + self.iconImageView.tintColor = theme.chat.inputPanel.inputControlColor.withAlphaComponent(1.0) + self.iconImageView.alpha = alpha * theme.chat.inputPanel.inputControlColor.alpha + } else { + self.iconImageView.tintColor = theme.chat.inputPanel.inputControlColor + self.iconImageView.alpha = alpha + } self.accessibilityLabel = accessibilityLabel @@ -128,8 +133,13 @@ final class AccessoryItemIconButton: HighlightTrackingButton, GlassBackgroundVie } self.iconImageView.image = image - self.iconImageView.tintColor = theme.chat.inputPanel.inputControlColor - self.iconImageView.alpha = alpha + if #available(iOS 26.0, *) { + self.iconImageView.tintColor = theme.chat.inputPanel.inputControlColor.withAlphaComponent(1.0) + self.iconImageView.alpha = alpha * theme.chat.inputPanel.inputControlColor.alpha + } else { + self.iconImageView.tintColor = theme.chat.inputPanel.inputControlColor + self.iconImageView.alpha = alpha + } self.accessibilityLabel = accessibilityLabel } @@ -308,7 +318,7 @@ final class AccessoryItemIconButton: HighlightTrackingButton, GlassBackgroundVie transition: .immediate, component: AnyComponent(LottieComponent( content: LottieComponent.AppBundleContent(name: animationName), - color: self.theme.chat.inputPanel.inputControlColor + color: self.theme.chat.inputPanel.inputControlColor.withAlphaComponent(1.0) )), environment: {}, containerSize: animationFrame.size @@ -322,7 +332,8 @@ final class AccessoryItemIconButton: HighlightTrackingButton, GlassBackgroundVie self.tintMask.addSubview(tintMaskAnimationView) } } - view.setMonochromaticEffect(tintColor: self.theme.chat.inputPanel.inputControlColor) + view.setMonochromaticEffect(tintColor: self.theme.chat.inputPanel.inputControlColor.withAlphaComponent(1.0)) + view.alpha = self.theme.chat.inputPanel.inputControlColor.alpha let animationFrameValue = CGRect(origin: CGPoint(x: animationFrame.minX + floor((animationFrame.width - animationSize.width) / 2.0), y: animationFrame.minY + floor((animationFrame.height - animationSize.height) / 2.0)), size: animationSize) view.frame = animationFrameValue if let tintMaskAnimationView = self.tintMaskAnimationView { diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index dcf2aef9bb..0ec4012d00 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -1597,13 +1597,13 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg if interfaceState.interfaceState.mediaDraftState != nil { self.attachmentButtonIcon.image = UIImage(bundleImageName: "Chat/Context Menu/Delete")?.withRenderingMode(.alwaysTemplate) - self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.inputControlColor + self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.panelControlColor } else if isEditingMedia { self.attachmentButtonIcon.image = PresentationResourcesChat.chatInputPanelEditAttachmentButtonImage(interfaceState.theme) - self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.inputControlColor + self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.panelControlColor } else { self.attachmentButtonIcon.image = PresentationResourcesChat.chatInputPanelAttachmentButtonImage(interfaceState.theme) - self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.inputControlColor + self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.panelControlColor } self.actionButtons.updateTheme(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) @@ -1630,13 +1630,13 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg if wasEditingMedia != isEditingMedia || hadMediaDraft != hasMediaDraft { if interfaceState.interfaceState.mediaDraftState != nil { self.attachmentButtonIcon.image = UIImage(bundleImageName: "Chat/Context Menu/Delete")?.withRenderingMode(.alwaysTemplate) - self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.inputControlColor + self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.panelControlColor } else if isEditingMedia { self.attachmentButtonIcon.image = PresentationResourcesChat.chatInputPanelEditAttachmentButtonImage(interfaceState.theme) self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.actionControlForegroundColor } else { self.attachmentButtonIcon.image = PresentationResourcesChat.chatInputPanelAttachmentButtonImage(interfaceState.theme) - self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.inputControlColor + self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.panelControlColor } } } diff --git a/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/ChatTextInputMediaRecordingButton.swift b/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/ChatTextInputMediaRecordingButton.swift index 4587bc4a00..f9169fc9f1 100644 --- a/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/ChatTextInputMediaRecordingButton.swift +++ b/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/ChatTextInputMediaRecordingButton.swift @@ -420,7 +420,7 @@ public final class ChatTextInputMediaRecordingButton: TGModernConversationInputM animationName = "anim_micToVideo" } - let animationTintColor = self.useDarkTheme ? .white : self.theme.chat.inputPanel.inputControlColor + let animationTintColor = self.useDarkTheme ? .white : self.theme.chat.inputPanel.panelControlColor let _ = self.animationView.update( transition: .immediate, component: AnyComponent(LottieComponent( diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift index f4d5396a52..1dc26e7f38 100644 --- a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift @@ -316,7 +316,7 @@ public class GlassBackgroundView: UIView { self.foregroundView = nil self.shadowView = nil } else { - self.backgroundNode = NavigationBackgroundNode(color: .black, enableBlur: true, customBlurRadius: 5.0) + self.backgroundNode = NavigationBackgroundNode(color: .black, enableBlur: true, customBlurRadius: 8.0) self.nativeView = nil self.nativeContainerView = nil self.nativeParamsView = nil @@ -682,9 +682,9 @@ public extension GlassBackgroundView { context.setFillColor(fillColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: inset, dy: inset)) - addShadow(false, CGPoint(x: 0.0, y: 0.0), 3.0, 0.0, UIColor(white: 1.0, alpha: 0.5), false) - addShadow(false, CGPoint(x: 3.0, y: -3.0), 2.0, 0.0, UIColor(white: 1.0, alpha: 0.25), false) - addShadow(false, CGPoint(x: -3.0, y: 3.0), 2.0, 0.0, UIColor(white: 1.0, alpha: 0.25), false) + addShadow(false, CGPoint(x: 0.0, y: 0.0), 3.0, 0.0, UIColor(white: 1.0, alpha: 0.25), false) + addShadow(false, CGPoint(x: 2.0, y: -2.0), 1.0, 0.0, UIColor(white: 1.0, alpha: 0.125), false) + addShadow(false, CGPoint(x: -2.0, y: 2.0), 1.0, 0.0, UIColor(white: 1.0, alpha: 0.125), false) } else { addShadow(true, CGPoint(), 16.0, 0.0, UIColor(white: 0.0, alpha: 0.08), false) @@ -822,7 +822,6 @@ public final class GlassBackgroundComponent: Component { public final class View: GlassBackgroundView { func update(component: GlassBackgroundComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.update(size: component.size, cornerRadius: component.cornerRadius, isDark: component.isDark, tintColor: component.tintColor, transition: transition) - self.frame = CGRect(origin: .zero, size: component.size) return component.size } diff --git a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift index 9b89c814e5..977fa836fe 100644 --- a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift +++ b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift @@ -501,7 +501,7 @@ public final class TabBarComponent: Component { let size = CGSize(width: min(availableSize.width, contentWidth), height: contentHeight) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) - self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.list.plainBackgroundColor.withMultipliedAlpha(0.75)), transition: transition) + self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: transition) if let nativeTabBar = self.nativeTabBar { transition.setFrame(view: nativeTabBar, frame: CGRect(origin: CGPoint(x: floor((size.width - nativeTabBar.bounds.width) * 0.5), y: 0.0), size: nativeTabBar.bounds.size)) diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift index 446347fd42..3826db5194 100644 --- a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift +++ b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift @@ -601,7 +601,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { component: AnyComponent( Image( image: state.image(.flip, theme: environment.theme), - tintColor: environment.theme.chat.inputPanel.inputControlColor, + tintColor: environment.theme.chat.inputPanel.panelControlColor, size: CGSize(width: 30.0, height: 30.0) ) ) @@ -646,7 +646,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { component: AnyComponent( LottieComponent( content: LottieComponent.AppBundleContent(name: flashIconName), - color: environment.theme.chat.inputPanel.inputControlColor, + color: environment.theme.chat.inputPanel.panelControlColor, startingPosition: !component.cameraState.flashModeDidChange ? .end : .begin, size: CGSize(width: 40.0, height: 40.0), loop: false, @@ -660,7 +660,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { component: AnyComponent( Image( image: state.image(.flash, theme: environment.theme), - tintColor: environment.theme.chat.inputPanel.inputControlColor, + tintColor: environment.theme.chat.inputPanel.panelControlColor, size: CGSize(width: 30.0, height: 30.0) ) ) @@ -732,7 +732,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { component: AnyComponent( BundleIconComponent( name: component.cameraState.isViewOnceEnabled ? "Media Gallery/ViewOnceEnabled" : "Media Gallery/ViewOnce", - tintColor: environment.theme.chat.inputPanel.inputControlColor + tintColor: environment.theme.chat.inputPanel.panelControlColor ) ) ) @@ -776,7 +776,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { component: AnyComponent( BundleIconComponent( name: "Chat/Input/Text/IconVideo", - tintColor: environment.theme.chat.inputPanel.inputControlColor + tintColor: environment.theme.chat.inputPanel.panelControlColor ) ) ) diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 61b7e8aef1..ebd0b85e24 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -1340,9 +1340,8 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.titleTopicsAccessoryPanelNode = nil } - var defaultLeftPanelWidth: CGFloat = 72.0 - defaultLeftPanelWidth += layout.safeInsets.left - let leftPanelLeftInset = defaultLeftPanelWidth - 72.0 + let defaultLeftPanelWidth: CGFloat = 72.0 + 8.0 + let leftPanelLeftInset = defaultLeftPanelWidth - (72.0 + 8.0) var leftPanelSize: CGSize? var dismissedLeftPanel: (component: AnyComponentWithIdentity, view: ComponentView)? @@ -1356,7 +1355,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.leftPanel = (leftPanelComponent, leftPanel.view) } - leftPanelSize = CGSize(width: defaultLeftPanelWidth, height: layout.size.height) + leftPanelSize = CGSize(width: defaultLeftPanelWidth + 8.0, height: layout.size.height) } else if let leftPanel = self.leftPanel { dismissedLeftPanel = leftPanel self.leftPanel = nil @@ -1378,7 +1377,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { titleAccessoryPanelNode.clipsToBounds = true } - let layoutResult = titleAccessoryPanelNode.updateLayout(width: layout.size.width, leftInset: leftPanelSize?.width ?? layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) + let layoutResult = titleAccessoryPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) titleAccessoryPanelHeight = layoutResult.insetHeight titleAccessoryPanelBackgroundHeight = layoutResult.backgroundHeight titleAccessoryPanelHitTestSlop = layoutResult.hitTestSlop @@ -1432,7 +1431,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { translationPanelNode.clipsToBounds = true } - let height = translationPanelNode.updateLayout(width: layout.size.width, leftInset: leftPanelSize?.width ?? layout.safeInsets.left, rightInset: layout.safeInsets.right, leftDisplayInset: leftPanelSize?.width ?? 0.0, transition: immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) + let height = translationPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, leftDisplayInset: 0.0, transition: immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) translationPanelHeight = height if immediatelyLayoutTranslationPanelNodeAndAnimateAppearance { translationPanelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) @@ -1496,7 +1495,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.titleAccessoryPanelContainer.addSubnode(adPanelNode) } - let height = adPanelNode.updateLayout(width: layout.size.width, leftInset: leftPanelSize?.width ?? layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition, interfaceState: self.chatPresentationInterfaceState) + let height = adPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition, interfaceState: self.chatPresentationInterfaceState) if let adMessage = self.chatPresentationInterfaceState.adMessage, let opaqueId = adMessage.adAttribute?.opaqueId { self.historyNode.markAdAsSeen(opaqueId: opaqueId) } @@ -1545,7 +1544,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.titleAccessoryPanelContainer.addSubnode(feePanelNode) } - let height = feePanelNode.updateLayout(width: layout.size.width, leftInset: leftPanelSize?.width ?? layout.safeInsets.left, rightInset: layout.safeInsets.right, leftDisplayInset: leftPanelSize?.width ?? 0.0, transition: animateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) + let height = feePanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, leftDisplayInset: 0.0, transition: animateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) feePanelHeight = height if transition.isAnimated && animateAppearance { @@ -1848,8 +1847,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { var titlePanelsContentOffset: CGFloat = 0.0 - let sidePanelTopInset: CGFloat = insets.top - var titleTopicsAccessoryPanelFrame: CGRect? if let _ = self.titleTopicsAccessoryPanelNode, let panelHeight = titleTopicsAccessoryPanelHeight { titleTopicsAccessoryPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: titlePanelsContentOffset), size: CGSize(width: layout.size.width, height: panelHeight)) @@ -1897,15 +1894,10 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { insets.top += panelHeight extraNavigationBarHeight += panelHeight } - - var extraNavigationBarLeftCutout: CGSize? - if let leftPanelSize { - extraNavigationBarLeftCutout = CGSize(width: leftPanelSize.width, height: navigationBarHeight) - } else { - extraNavigationBarLeftCutout = CGSize(width: 0.0, height: navigationBarHeight) - } - updateExtraNavigationBarBackgroundHeight(extraNavigationBarHeight, extraNavigationBarHitTestSlop, extraNavigationBarLeftCutout, transition) + updateExtraNavigationBarBackgroundHeight(extraNavigationBarHeight, extraNavigationBarHitTestSlop, nil, transition) + + let sidePanelTopInset: CGFloat = insets.top let contentBounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width - wrappingInsets.left - wrappingInsets.right, height: layout.size.height - wrappingInsets.top - wrappingInsets.bottom) @@ -2543,31 +2535,22 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { ChatSidePanelEnvironment(insets: UIEdgeInsets( top: 0.0, left: leftPanelLeftInset, - bottom: containerInsets.bottom + inputPanelsHeight, + bottom: containerInsets.bottom + inputPanelsHeight + 8.0, right: 0.0 )) }, - containerSize: CGSize(width: leftPanelSize.width, height: leftPanelSize.height - sidePanelTopInset) + containerSize: CGSize(width: defaultLeftPanelWidth, height: leftPanelSize.height - sidePanelTopInset) ) - let leftPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: sidePanelTopInset), size: leftPanelSize) + let leftPanelFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: sidePanelTopInset), size: leftPanelSize) if let leftPanelView = leftPanel.view.view { if leftPanelView.superview == nil { self.leftPanelContainer.view.addSubview(leftPanelView) } if immediatelyLayoutLeftPanelNodeAndAnimateAppearance { - leftPanelView.frame = leftPanelFrame.offsetBy(dx: -leftPanelSize.width, dy: 0.0) - - if self.titleTopicsAccessoryPanelNode != nil || dismissedTitleTopicsAccessoryPanelNode != nil { - if let leftPanelView = leftPanelView as? ChatSideTopicsPanel.View { - leftPanelView.updateGlobalOffset(globalOffset: -leftPanelSize.width, transition: ComponentTransition(transition)) - } - } + leftPanelView.frame = leftPanelFrame.offsetBy(dx: -leftPanelSize.width - 16.0, dy: 0.0) } transition.updateFrame(view: leftPanelView, frame: leftPanelFrame) - if let leftPanelView = leftPanelView as? ChatSideTopicsPanel.View { - leftPanelView.updateGlobalOffset(globalOffset: 0.0, transition: ComponentTransition(transition)) - } } } if let dismissedLeftPanel, let dismissedLeftPanelView = dismissedLeftPanel.view.view { @@ -2584,14 +2567,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { }, containerSize: CGSize(width: defaultLeftPanelWidth, height: layout.size.height - sidePanelTopInset - (containerInsets.bottom + inputPanelsHeight)) ) - transition.updateFrame(view: dismissedLeftPanelView, frame: CGRect(origin: CGPoint(x: -dismissedLeftPanelSize.width, y: sidePanelTopInset), size: dismissedLeftPanelSize), completion: { [weak dismissedLeftPanelView] _ in + transition.updateFrame(view: dismissedLeftPanelView, frame: CGRect(origin: CGPoint(x: -layout.safeInsets.left - dismissedLeftPanelSize.width - 16.0, y: sidePanelTopInset), size: dismissedLeftPanelSize), completion: { [weak dismissedLeftPanelView] _ in dismissedLeftPanelView?.removeFromSuperview() }) - if let dismissedLeftPanelView = dismissedLeftPanelView as? ChatSideTopicsPanel.View { - if self.titleTopicsAccessoryPanelNode != nil { - dismissedLeftPanelView.updateGlobalOffset(globalOffset: -dismissedLeftPanelSize.width, transition: ComponentTransition(transition)) - } - } } if let navigationBarBackgroundContent = self.navigationBarBackgroundContent { @@ -2612,12 +2590,8 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if let titleTopicsAccessoryPanelNode = self.titleTopicsAccessoryPanelNode, let titleTopicsAccessoryPanelFrame, (immediatelyLayoutTitleTopicsAccessoryPanelNodeAndAnimateAppearance || !titleTopicsAccessoryPanelNode.frame.equalTo(titleTopicsAccessoryPanelFrame)) { if immediatelyLayoutTitleTopicsAccessoryPanelNodeAndAnimateAppearance { titleTopicsAccessoryPanelNode.frame = titleTopicsAccessoryPanelFrame.offsetBy(dx: 0.0, dy: -titleTopicsAccessoryPanelFrame.height) - if self.leftPanel != nil || dismissedLeftPanel != nil { - titleTopicsAccessoryPanelNode.updateGlobalOffset(globalOffset: -titleTopicsAccessoryPanelFrame.height, transition: .immediate) - } ComponentTransition(transition).setFrame(view: titleTopicsAccessoryPanelNode.view, frame: titleTopicsAccessoryPanelFrame) - titleTopicsAccessoryPanelNode.updateGlobalOffset(globalOffset: 0.0, transition: ComponentTransition(transition)) } else { let previousFrame = titleTopicsAccessoryPanelNode.frame titleTopicsAccessoryPanelNode.frame = titleTopicsAccessoryPanelFrame @@ -2759,9 +2733,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { transition.updateFrame(node: dismissedTitleTopicsAccessoryPanelNode, frame: dismissedTopPanelFrame, completion: { [weak dismissedTitleTopicsAccessoryPanelNode] _ in dismissedTitleTopicsAccessoryPanelNode?.removeFromSupernode() }) - if self.leftPanel != nil { - dismissedTitleTopicsAccessoryPanelNode.updateGlobalOffset(globalOffset: -dismissedTopPanelFrame.height, transition: ComponentTransition(transition)) - } } if let dismissedTitleAccessoryPanelNode { diff --git a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift index e997379ea8..57811058af 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift @@ -95,7 +95,7 @@ class ChatHistoryNavigationButtonNode: ContextControllerSourceNode { self.buttonNode.view.addSubview(self.backgroundView) self.backgroundView.frame = CGRect(origin: CGPoint(), size: size) self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: .immediate) - self.imageView.tintColor = theme.chat.inputPanel.inputControlColor + self.imageView.tintColor = theme.chat.inputPanel.panelControlColor self.backgroundView.contentView.addSubview(self.imageView) self.imageView.frame = CGRect(origin: CGPoint(), size: size) @@ -111,7 +111,7 @@ class ChatHistoryNavigationButtonNode: ContextControllerSourceNode { self.theme = theme self.backgroundView.update(size: self.backgroundView.bounds.size, cornerRadius: self.backgroundView.bounds.size.height * 0.5, isDark: theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: .immediate) - self.imageView.tintColor = theme.chat.inputPanel.inputControlColor + self.imageView.tintColor = theme.chat.inputPanel.panelControlColor switch self.type { case .down: diff --git a/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift index eaf18a9d40..d361288fc4 100644 --- a/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift @@ -308,7 +308,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { component: AnyComponent(PlainButtonComponent( content: AnyComponent(AnimatedTextComponent( font: Font.regular(15.0), - color: params.interfaceState.theme.chat.inputPanel.inputControlColor, + color: params.interfaceState.theme.chat.inputPanel.panelControlColor, items: modeButtonTitle )), effectAlignment: .right, @@ -362,7 +362,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { component: AnyComponent(PlainButtonComponent( content: AnyComponent(BundleIconComponent( name: "Chat/Input/Search/Calendar", - tintColor: params.interfaceState.theme.chat.inputPanel.inputControlColor + tintColor: params.interfaceState.theme.chat.inputPanel.panelControlColor )), effectAlignment: .center, minSize: CGSize(width: 40.0, height: 40.0), @@ -419,7 +419,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { component: AnyComponent(PlainButtonComponent( content: AnyComponent(BundleIconComponent( name: "Chat/Input/Search/Members", - tintColor: params.interfaceState.theme.chat.inputPanel.inputControlColor + tintColor: params.interfaceState.theme.chat.inputPanel.panelControlColor )), effectAlignment: .center, minSize: CGSize(width: 40.0, height: 40.0),