diff --git a/Rosetta/Features/Chats/ChatList/UIKit/ChatListCollectionController.swift b/Rosetta/Features/Chats/ChatList/UIKit/ChatListCollectionController.swift index 09c63c2..a09c52a 100644 --- a/Rosetta/Features/Chats/ChatList/UIKit/ChatListCollectionController.swift +++ b/Rosetta/Features/Chats/ChatList/UIKit/ChatListCollectionController.swift @@ -41,6 +41,8 @@ final class ChatListCollectionController: UIViewController { private var lastReportedExpansion: CGFloat = 1.0 private var lastReportedPinnedHeaderFraction: CGFloat = -1.0 private let searchCollapseDistance: CGFloat = 54 + /// Extra top offset for custom header bar (nav bar is hidden) + var customHeaderBarHeight: CGFloat = 44 private var searchHeaderExpansion: CGFloat = 1.0 private var hasInitializedTopOffset = false private var isPinnedFractionReportScheduled = false @@ -121,7 +123,7 @@ final class ChatListCollectionController: UIViewController { private func applyInsets() { guard collectionView != nil else { return } let oldTopInset = collectionView.contentInset.top - let topInset = view.safeAreaInsets.top + (searchCollapseDistance * searchHeaderExpansion) + let topInset = view.safeAreaInsets.top + customHeaderBarHeight + (searchCollapseDistance * searchHeaderExpansion) let bottomInset = chatListBottomInset collectionView.contentInset.top = topInset collectionView.contentInset.bottom = bottomInset @@ -465,7 +467,7 @@ extension ChatListCollectionController: UICollectionViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { // Only react to user-driven scroll, not programmatic/layout changes guard scrollView.isDragging || scrollView.isDecelerating else { return } - let offset = scrollView.contentOffset.y + view.safeAreaInsets.top + searchCollapseDistance + let offset = scrollView.contentOffset.y + view.safeAreaInsets.top + customHeaderBarHeight + searchCollapseDistance let expansion = max(0.0, min(1.0, 1.0 - offset / searchCollapseDistance)) if abs(expansion - lastReportedExpansion) > 0.005 { lastReportedExpansion = expansion @@ -482,11 +484,11 @@ extension ChatListCollectionController: UICollectionViewDelegate { // Telegram snap-to-edge: if search bar is partially visible, snap to // fully visible (>50%) or fully hidden (<50%). guard lastReportedExpansion > 0.0 && lastReportedExpansion < 1.0 else { return } - let safeTop = view.safeAreaInsets.top + let headerTop = view.safeAreaInsets.top + customHeaderBarHeight if lastReportedExpansion < 0.5 { - targetContentOffset.pointee.y = -safeTop + targetContentOffset.pointee.y = -headerTop } else { - targetContentOffset.pointee.y = -(safeTop + searchCollapseDistance) + targetContentOffset.pointee.y = -(headerTop + searchCollapseDistance) } } diff --git a/Rosetta/Features/Chats/ChatList/UIKit/ChatListUIKitView.swift b/Rosetta/Features/Chats/ChatList/UIKit/ChatListUIKitView.swift index a04b1bb..41efc0b 100644 --- a/Rosetta/Features/Chats/ChatList/UIKit/ChatListUIKitView.swift +++ b/Rosetta/Features/Chats/ChatList/UIKit/ChatListUIKitView.swift @@ -88,7 +88,7 @@ final class ChatListRootViewController: UIViewController, UINavigationController private var typingDialogs: [String: Set] = [:] private var currentSearchQuery = "" private var searchResultUsersByKey: [String: SearchUser] = [:] - private let searchTopSpacing: CGFloat = 5 + private let searchTopSpacing: CGFloat = 10 private let searchBottomSpacing: CGFloat = 5 private let searchHeaderHeight: CGFloat = 44 private var searchChromeHeight: CGFloat { @@ -275,8 +275,8 @@ final class ChatListRootViewController: UIViewController, UINavigationController NSLayoutConstraint.activate([ top, - searchHeaderView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - searchHeaderView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + searchHeaderView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16), + searchHeaderView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16), height, ]) @@ -445,20 +445,20 @@ final class ChatListRootViewController: UIViewController, UINavigationController guard abs(clamped - lastSearchExpansion) > 0.003 else { return } lastSearchExpansion = clamped - // Telegram non-linear: search bar stays fully visible for first 27% of scroll - let visibleProgress = max(0.0, min(1.0, (clamped - 0.267) / 0.733)) - - // Structural: use raw clamped - searchHeaderTopConstraint?.constant = searchTopSpacing * clamped + // Structural: top stays fixed, height collapses (Telegram: 60*progress, we use 44) + searchHeaderTopConstraint?.constant = headerBarHeight + searchTopSpacing searchHeaderHeightConstraint?.constant = searchHeaderHeight * clamped listController.setSearchHeaderExpansion(clamped) - // Visual: use non-linear visibleProgress - searchHeaderView.alpha = visibleProgress - searchHeaderView.isUserInteractionEnabled = visibleProgress > 0.2 - let yShift = -8.0 * (1.0 - clamped) - searchHeaderView.transform = CGAffineTransform(translationX: 0, y: yShift) - .scaledBy(x: 1.0, y: 0.92 + 0.08 * visibleProgress) + // Telegram animation: NO scale transform, NO whole-view alpha. + // Height reduction + clipping handles the collapse. + // Only content (text/icon) fades separately. + searchHeaderView.transform = .identity + searchHeaderView.alpha = 1.0 + searchHeaderView.isUserInteractionEnabled = clamped > 0.2 + + // Update internal content alpha + corner radius (Telegram behavior) + searchHeaderView.updateExpansionProgress(clamped) updateNavigationBlurHeight() updateNavigationBarBlur(progress: 1.0 - clamped) @@ -838,12 +838,30 @@ private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate { setSearchActive(false, animated: animated, clearText: clearText) } + /// Telegram-style expansion animation: + /// - Corner radius scales with height (pill shape maintained) + /// - Text/icon alpha: invisible until 77% expanded, then ramps to 1.0 + /// - Background: always visible (controlled by height clipping) + func updateExpansionProgress(_ progress: CGFloat) { + let currentHeight = searchBarHeight * progress + // Dynamic corner radius — Telegram: height * 0.5 + capsuleView.layer.cornerRadius = max(0, currentHeight * 0.5) + + // Telegram inner content alpha: 0 until 77%, then ramps to 1 + let innerAlpha = max(0.0, min(1.0, (progress - 0.77) / 0.23)) + placeholderStack.alpha = isSearchActive ? 0 : innerAlpha + placeholderIcon.alpha = innerAlpha + placeholderLabel.alpha = innerAlpha + } + + private let searchBarHeight: CGFloat = 44 + private func setupUI() { translatesAutoresizingMaskIntoConstraints = false capsuleView.translatesAutoresizingMaskIntoConstraints = false capsuleView.layer.cornerRadius = 22 - capsuleView.layer.borderWidth = 0.5 + capsuleView.layer.borderWidth = 0 capsuleView.clipsToBounds = true addSubview(capsuleView) @@ -901,7 +919,7 @@ private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate { capsuleView.leadingAnchor.constraint(equalTo: leadingAnchor), capsuleView.topAnchor.constraint(equalTo: topAnchor), capsuleView.bottomAnchor.constraint(equalTo: bottomAnchor), - capsuleView.trailingAnchor.constraint(equalTo: cancelButton.leadingAnchor, constant: -8), + capsuleView.trailingAnchor.constraint(equalTo: cancelButton.leadingAnchor), cancelButton.trailingAnchor.constraint(equalTo: trailingAnchor), cancelButton.centerYAnchor.constraint(equalTo: centerYAnchor), @@ -921,13 +939,22 @@ private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate { } private func applyColors() { - capsuleView.backgroundColor = UIColor(RosettaColors.Adaptive.searchBarFill) - capsuleView.layer.borderColor = UIColor(RosettaColors.Adaptive.searchBarBorder).cgColor - placeholderLabel.textColor = .gray - placeholderIcon.tintColor = .gray - activeIcon.tintColor = .gray - textField.textColor = UIColor(RosettaColors.Adaptive.text) - inlineClearButton.tintColor = .gray + // Telegram exact colors from DefaultDarkPresentationTheme / DefaultDayPresentationTheme + let isDark = traitCollection.userInterfaceStyle == .dark + // regularSearchBarColor: dark #272728, light #e9e9e9 + capsuleView.backgroundColor = isDark + ? UIColor(red: 0x27/255.0, green: 0x27/255.0, blue: 0x28/255.0, alpha: 1.0) + : UIColor(red: 0xe9/255.0, green: 0xe9/255.0, blue: 0xe9/255.0, alpha: 1.0) + // inputPlaceholderTextColor: dark #8f8f8f, light #8e8e93 + let placeholderColor = isDark + ? UIColor(red: 0x8f/255.0, green: 0x8f/255.0, blue: 0x8f/255.0, alpha: 1.0) + : UIColor(red: 0x8e/255.0, green: 0x8e/255.0, blue: 0x93/255.0, alpha: 1.0) + placeholderLabel.textColor = placeholderColor + placeholderIcon.tintColor = placeholderColor + activeIcon.tintColor = placeholderColor + // inputTextColor: dark #ffffff, light #000000 + textField.textColor = isDark ? .white : .black + inlineClearButton.tintColor = placeholderColor cancelButton.setTitleColor(UIColor(RosettaColors.primaryBlue), for: .normal) } @@ -1012,8 +1039,11 @@ private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate { private final class ChatListHeaderBlurView: UIView { - private let edgeEffectView = UIView() - private let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) + // Tint overlay — shows pinned section background color via gradient mask + private let tintView = UIView() + private let tintMaskView = UIImageView() + // CABackdropLayer — captures content behind and applies subtle blur + private var backdropLayer: CALayer? private let fadeMaskLayer = CAGradientLayer() private var plainBackgroundColor: UIColor = .black private var pinnedBackgroundColor: UIColor = .black @@ -1024,27 +1054,27 @@ private final class ChatListHeaderBlurView: UIView { override init(frame: CGRect) { super.init(frame: frame) isUserInteractionEnabled = false - clipsToBounds = true - edgeEffectView.translatesAutoresizingMaskIntoConstraints = false - blurView.translatesAutoresizingMaskIntoConstraints = false + // Backdrop blur layer — very subtle (radius 1.0), no colorMatrix + if let backdrop = BackdropLayerHelper.createBackdropLayer() { + backdrop.delegate = BackdropLayerDelegate.shared + BackdropLayerHelper.setScale(backdrop, scale: 0.5) + if let blur = CALayer.blurFilter() { + blur.setValue(1.0 as NSNumber, forKey: "inputRadius") + backdrop.filters = [blur] + } + layer.addSublayer(backdrop) + self.backdropLayer = backdrop + } - addSubview(edgeEffectView) - addSubview(blurView) - - NSLayoutConstraint.activate([ - edgeEffectView.topAnchor.constraint(equalTo: topAnchor), - edgeEffectView.leadingAnchor.constraint(equalTo: leadingAnchor), - edgeEffectView.trailingAnchor.constraint(equalTo: trailingAnchor), - edgeEffectView.bottomAnchor.constraint(equalTo: bottomAnchor), - - blurView.topAnchor.constraint(equalTo: topAnchor), - blurView.leadingAnchor.constraint(equalTo: leadingAnchor), - blurView.trailingAnchor.constraint(equalTo: trailingAnchor), - blurView.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) + // Tint view with gradient mask (for pinned section color) + tintView.mask = tintMaskView + tintView.alpha = 0.85 + addSubview(tintView) + // Gradient fade mask on the whole view layer.mask = fadeMaskLayer + applyAdaptiveColors() } @@ -1060,14 +1090,16 @@ private final class ChatListHeaderBlurView: UIView { override func layoutSubviews() { super.layoutSubviews() + backdropLayer?.frame = bounds + tintView.frame = bounds + tintMaskView.frame = bounds updateFadeMask() + updateTintMask() } private func applyAdaptiveColors() { plainBackgroundColor = UIColor(RosettaColors.Adaptive.background) pinnedBackgroundColor = UIColor(RosettaColors.Adaptive.pinnedSectionBackground) - blurView.effect = UIBlurEffect(style: .light) - configureTelegramBlurFilters() updateEdgeEffectColor() updateChromeOpacity() } @@ -1075,25 +1107,20 @@ private final class ChatListHeaderBlurView: UIView { private func updateEdgeEffectColor() { let effectivePinnedFraction = isSearchCurrentlyActive ? 0.0 : currentPinnedFraction let resolved = plainBackgroundColor.mixedWith(pinnedBackgroundColor, alpha: effectivePinnedFraction) - edgeEffectView.backgroundColor = resolved + tintView.backgroundColor = resolved } private func updateChromeOpacity() { let clamped = max(0.0, min(1.0, currentProgress)) - edgeEffectView.alpha = clamped - blurView.alpha = 0.85 * clamped + // Backdrop blur is always present — its visibility depends on content behind. + // Tint overlay fades in with scroll progress. + tintView.alpha = 0.85 * clamped } - private func configureTelegramBlurFilters() { - guard let sublayer = blurView.layer.sublayers?.first, - let filters = sublayer.filters else { return } - sublayer.backgroundColor = nil - sublayer.isOpaque = false - let allowedKeys: Set = ["gaussianBlur", "colorSaturate"] - sublayer.filters = filters.filter { filter in - guard let obj = filter as? NSObject else { return true } - return allowedKeys.contains(String(describing: obj)) - } + private func updateTintMask() { + let height = max(1, bounds.height) + let edgeSize = min(54.0, height) + tintMaskView.image = VariableBlurEdgeView.generateEdgeGradient(baseHeight: edgeSize) } private func updateFadeMask() { @@ -1118,34 +1145,13 @@ private final class ChatListHeaderBlurView: UIView { private final class ChatListToolbarGlassCapsuleView: UIView { - private let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterialDark)) - private let tintView = UIView() + private let glassView = TelegramGlassUIView(frame: .zero) override init(frame: CGRect) { super.init(frame: frame) isUserInteractionEnabled = false - clipsToBounds = true - - blurView.translatesAutoresizingMaskIntoConstraints = false - tintView.translatesAutoresizingMaskIntoConstraints = false - addSubview(blurView) - addSubview(tintView) - - NSLayoutConstraint.activate([ - blurView.topAnchor.constraint(equalTo: topAnchor), - blurView.leadingAnchor.constraint(equalTo: leadingAnchor), - blurView.trailingAnchor.constraint(equalTo: trailingAnchor), - blurView.bottomAnchor.constraint(equalTo: bottomAnchor), - - tintView.topAnchor.constraint(equalTo: topAnchor), - tintView.leadingAnchor.constraint(equalTo: leadingAnchor), - tintView.trailingAnchor.constraint(equalTo: trailingAnchor), - tintView.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - - layer.borderWidth = 1.0 / UIScreen.main.scale - applyColors() - configureTelegramBlurFilters() + clipsToBounds = false + addSubview(glassView) } required init?(coder: NSCoder) { @@ -1154,34 +1160,9 @@ private final class ChatListToolbarGlassCapsuleView: UIView { override func layoutSubviews() { super.layoutSubviews() - applyColors() - layer.cornerRadius = bounds.height * 0.5 - configureTelegramBlurFilters() - } - - private func applyColors() { - let isDark = traitCollection.userInterfaceStyle == .dark - blurView.effect = UIBlurEffect(style: isDark ? .systemChromeMaterialDark : .systemChromeMaterialLight) - blurView.alpha = isDark ? 0.88 : 0.82 - - tintView.backgroundColor = isDark - ? UIColor(white: 0.0, alpha: 0.34) - : UIColor(white: 1.0, alpha: 0.28) - layer.borderColor = isDark - ? UIColor.white.withAlphaComponent(0.12).cgColor - : UIColor.black.withAlphaComponent(0.10).cgColor - } - - private func configureTelegramBlurFilters() { - guard let sublayer = blurView.layer.sublayers?.first, - let filters = sublayer.filters else { return } - sublayer.backgroundColor = nil - sublayer.isOpaque = false - let allowedKeys: Set = ["gaussianBlur", "colorSaturate"] - sublayer.filters = filters.filter { filter in - guard let obj = filter as? NSObject else { return true } - return allowedKeys.contains(String(describing: obj)) - } + glassView.frame = bounds + glassView.fixedCornerRadius = bounds.height * 0.5 + glassView.updateGlass() } } @@ -1271,7 +1252,7 @@ private final class ChatListToolbarDualActionButton: UIView { addButton.accessibilityLabel = "Add" composeButton.accessibilityLabel = "Compose" - let iconConfig = UIImage.SymbolConfiguration(pointSize: 16, weight: .medium) + let iconConfig = UIImage.SymbolConfiguration(pointSize: 18, weight: .medium) let addIcon = UIImage(named: "toolbar-add-chat")?.withRenderingMode(.alwaysTemplate) ?? UIImage(systemName: "plus", withConfiguration: iconConfig) let composeIcon = UIImage(named: "toolbar-compose")?.withRenderingMode(.alwaysTemplate) @@ -1301,12 +1282,12 @@ private final class ChatListToolbarDualActionButton: UIView { addButton.leadingAnchor.constraint(equalTo: leadingAnchor), addButton.topAnchor.constraint(equalTo: topAnchor), addButton.bottomAnchor.constraint(equalTo: bottomAnchor), - addButton.widthAnchor.constraint(equalToConstant: 38), + addButton.widthAnchor.constraint(equalToConstant: 44), composeButton.trailingAnchor.constraint(equalTo: trailingAnchor), composeButton.topAnchor.constraint(equalTo: topAnchor), composeButton.bottomAnchor.constraint(equalTo: bottomAnchor), - composeButton.widthAnchor.constraint(equalToConstant: 38), + composeButton.widthAnchor.constraint(equalToConstant: 44), dividerView.centerXAnchor.constraint(equalTo: centerXAnchor), dividerView.centerYAnchor.constraint(equalTo: centerYAnchor), @@ -1314,7 +1295,7 @@ private final class ChatListToolbarDualActionButton: UIView { dividerView.heightAnchor.constraint(equalToConstant: 20), heightAnchor.constraint(equalToConstant: 44), - widthAnchor.constraint(equalToConstant: 76), + widthAnchor.constraint(equalToConstant: 88), ]) self.frame = CGRect(origin: .zero, size: intrinsicContentSize) @@ -1325,7 +1306,7 @@ private final class ChatListToolbarDualActionButton: UIView { } override var intrinsicContentSize: CGSize { - CGSize(width: 76, height: 44) + CGSize(width: 88, height: 44) } @objc private func handleAddTapped() {