Фикс: починил индикацию прочитанных сообщений после миграции на UIKit
This commit is contained in:
@@ -59,6 +59,12 @@ final class ChatDetailViewController: UIViewController {
|
||||
// MARK: - Edge Effects
|
||||
|
||||
private let topEdgeEffectView = VariableBlurEdgeView(frame: .zero)
|
||||
private let bottomEdgeGradientView: UIView = {
|
||||
let v = UIView()
|
||||
v.isUserInteractionEnabled = false
|
||||
return v
|
||||
}()
|
||||
private let bottomGradientLayer = CAGradientLayer()
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
@@ -83,6 +89,13 @@ final class ChatDetailViewController: UIViewController {
|
||||
setupEdgeEffects()
|
||||
wireCellActions()
|
||||
wireViewModelSubscriptions()
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(appDidBecomeActive),
|
||||
name: UIApplication.didBecomeActiveNotification,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
@@ -185,6 +198,17 @@ final class ChatDetailViewController: UIViewController {
|
||||
controller.loadViewIfNeeded()
|
||||
|
||||
messageListController = controller
|
||||
|
||||
// Fix date pill sticky offset for floating header
|
||||
messageListController.topStickyOffset = headerBarHeight
|
||||
|
||||
// Reparent pill overlay: above edge effect (z=40), below toolbar (z=55)
|
||||
let overlay = messageListController.datePillOverlay
|
||||
overlay.removeFromSuperview()
|
||||
overlay.layer.zPosition = 45
|
||||
overlay.frame = view.bounds
|
||||
overlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
view.addSubview(overlay)
|
||||
}
|
||||
|
||||
// MARK: - Toolbar Setup (glass capsules as direct subviews)
|
||||
@@ -266,12 +290,45 @@ final class ChatDetailViewController: UIViewController {
|
||||
let isDark = traitCollection.userInterfaceStyle == .dark
|
||||
topEdgeEffectView.setTintColor(isDark ? .black : .white)
|
||||
view.addSubview(topEdgeEffectView)
|
||||
|
||||
// Bottom gradient: simple tint fade (no blur) above composer area
|
||||
bottomEdgeGradientView.layer.zPosition = 40
|
||||
bottomGradientLayer.startPoint = CGPoint(x: 0.5, y: 0)
|
||||
bottomGradientLayer.endPoint = CGPoint(x: 0.5, y: 1)
|
||||
updateBottomGradientColors()
|
||||
bottomEdgeGradientView.layer.addSublayer(bottomGradientLayer)
|
||||
view.addSubview(bottomEdgeGradientView)
|
||||
}
|
||||
|
||||
private func updateEdgeEffectFrames() {
|
||||
let edgeHeight = view.safeAreaInsets.top + headerBarHeight + 14
|
||||
topEdgeEffectView.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: edgeHeight)
|
||||
topEdgeEffectView.update(size: topEdgeEffectView.bounds.size, edgeSize: 54, contentAlpha: 0.85)
|
||||
|
||||
// Bottom gradient: tint fade below composer, in bottom safe area
|
||||
let bottomSafe = view.safeAreaInsets.bottom
|
||||
if bottomSafe > 0 {
|
||||
bottomEdgeGradientView.isHidden = false
|
||||
bottomEdgeGradientView.frame = CGRect(
|
||||
x: 0,
|
||||
y: view.bounds.height - bottomSafe,
|
||||
width: view.bounds.width,
|
||||
height: bottomSafe
|
||||
)
|
||||
bottomGradientLayer.frame = bottomEdgeGradientView.bounds
|
||||
} else {
|
||||
bottomEdgeGradientView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
private func updateBottomGradientColors() {
|
||||
let bg = traitCollection.userInterfaceStyle == .dark
|
||||
? UIColor.black
|
||||
: UIColor.white
|
||||
bottomGradientLayer.colors = [
|
||||
bg.withAlphaComponent(0).cgColor,
|
||||
bg.withAlphaComponent(1).cgColor,
|
||||
]
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
@@ -279,6 +336,7 @@ final class ChatDetailViewController: UIViewController {
|
||||
if traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle {
|
||||
let isDark = traitCollection.userInterfaceStyle == .dark
|
||||
topEdgeEffectView.setTintColor(isDark ? .black : .white)
|
||||
updateBottomGradientColors()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,7 +344,13 @@ final class ChatDetailViewController: UIViewController {
|
||||
|
||||
private func wireMessageListCallbacks(_ controller: NativeMessageListController) {
|
||||
controller.onScrollToBottomVisibilityChange = { [weak self] atBottom in
|
||||
self?.isAtBottom = atBottom
|
||||
guard let self else { return }
|
||||
self.isAtBottom = atBottom
|
||||
SessionManager.shared.resetIdleTimer()
|
||||
self.updateReadEligibility()
|
||||
if atBottom {
|
||||
self.markDialogAsRead()
|
||||
}
|
||||
}
|
||||
controller.onPaginationTrigger = { [weak self] in
|
||||
Task { await self?.viewModel.loadMore() }
|
||||
@@ -360,6 +424,12 @@ final class ChatDetailViewController: UIViewController {
|
||||
DispatchQueue.main.async {
|
||||
controller.scrollToBottom(animated: true)
|
||||
}
|
||||
// Mark incoming auto-scrolled messages as read (SwiftUI onNewMessageAutoScroll parity)
|
||||
if isViewActive && !lastIsOutgoing
|
||||
&& !route.isSavedMessages && !route.isSystemAccount {
|
||||
updateReadEligibility()
|
||||
markDialogAsRead()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -798,6 +868,16 @@ final class ChatDetailViewController: UIViewController {
|
||||
MessageRepository.shared.setDialogReadEligible(route.publicKey, isEligible: isViewActive && isAtBottom)
|
||||
}
|
||||
|
||||
@objc private func appDidBecomeActive() {
|
||||
guard isViewActive else { return }
|
||||
SessionManager.shared.resetIdleTimer()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { [weak self] in
|
||||
guard let self, self.isViewActive else { return }
|
||||
self.updateReadEligibility()
|
||||
self.markDialogAsRead()
|
||||
}
|
||||
}
|
||||
|
||||
private func activateDialog() {
|
||||
if DialogRepository.shared.dialogs[route.publicKey] != nil {
|
||||
DialogRepository.shared.ensureDialog(
|
||||
|
||||
@@ -143,6 +143,16 @@ final class NativeMessageListController: UIViewController {
|
||||
private var dateHideTimer: Timer?
|
||||
private var areDatePillsVisible = false
|
||||
private var animatePillFrames = false
|
||||
/// Extra top offset for sticky pills when parent has floating header overlay.
|
||||
/// Default 0 preserves behavior for SwiftUI host (where nav bar inflates safe area).
|
||||
var topStickyOffset: CGFloat = 0
|
||||
/// Container for floating date pills — parent VC can reparent for z-ordering.
|
||||
let datePillOverlay: UIView = {
|
||||
let v = UIView()
|
||||
v.isUserInteractionEnabled = false
|
||||
v.backgroundColor = .clear
|
||||
return v
|
||||
}()
|
||||
|
||||
// MARK: - Empty State (UIKit-managed, animates with keyboard)
|
||||
private var emptyStateHosting: UIHostingController<EmptyChatContent>?
|
||||
@@ -826,10 +836,14 @@ final class NativeMessageListController: UIViewController {
|
||||
}
|
||||
|
||||
private func setupFloatingDateHeader() {
|
||||
datePillOverlay.frame = view.bounds
|
||||
datePillOverlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
view.addSubview(datePillOverlay)
|
||||
|
||||
// Pre-create pool of 4 pills (max visible date sections at once).
|
||||
for _ in 0..<4 {
|
||||
let pill = makeDatePill()
|
||||
view.addSubview(pill.container)
|
||||
datePillOverlay.addSubview(pill.container)
|
||||
datePillPool.append(pill)
|
||||
}
|
||||
}
|
||||
@@ -846,7 +860,7 @@ final class NativeMessageListController: UIViewController {
|
||||
|
||||
let pillH: CGFloat = 24
|
||||
let hPad: CGFloat = 7
|
||||
let stickyY = view.safeAreaInsets.top + 8
|
||||
let stickyY = view.safeAreaInsets.top + topStickyOffset + 8
|
||||
|
||||
// 1. Group visible cells by date → section ranges in screen coords.
|
||||
struct DateSection {
|
||||
@@ -887,11 +901,7 @@ final class NativeMessageListController: UIViewController {
|
||||
// 2. Expand pool if more sections than pills (short chats spanning many days).
|
||||
while datePillPool.count < sections.count {
|
||||
let pill = makeDatePill()
|
||||
if let composer = composerView {
|
||||
view.insertSubview(pill.container, belowSubview: composer)
|
||||
} else {
|
||||
view.addSubview(pill.container)
|
||||
}
|
||||
datePillOverlay.addSubview(pill.container)
|
||||
datePillPool.append(pill)
|
||||
}
|
||||
|
||||
|
||||
@@ -194,11 +194,10 @@ private extension ChatListSearchContent {
|
||||
)
|
||||
}
|
||||
}
|
||||
// Telegram parity: subtitle shows online status, not @username
|
||||
if !isSelf {
|
||||
let dialog = DialogRepository.shared.dialogs[user.publicKey]
|
||||
let isOnline = dialog?.isOnline ?? false
|
||||
Text(isOnline ? "online" : "last seen recently")
|
||||
Text(isOnline ? "online" : "offline")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(isOnline
|
||||
? RosettaColors.primaryBlue
|
||||
|
||||
@@ -222,8 +222,9 @@ final class ChatListRootViewController: UIViewController, UINavigationController
|
||||
editButtonControl.isUserInteractionEnabled = true
|
||||
rightButtonsControl.isUserInteractionEnabled = true
|
||||
toolbarTitleView.isUserInteractionEnabled = true
|
||||
// Restore search bar position and tear down overlay
|
||||
// Restore search bar position, blur, and tear down overlay
|
||||
searchHeaderTopConstraint?.constant = headerBarHeight + searchTopSpacing
|
||||
navigationBlurView.isHidden = false
|
||||
teardownSearchOverlayImmediately()
|
||||
}
|
||||
}
|
||||
@@ -308,7 +309,9 @@ final class ChatListRootViewController: UIViewController, UINavigationController
|
||||
searchHeaderView.onActiveChanged = { [weak self] active in
|
||||
guard let self else { return }
|
||||
self.applySearchExpansion(1.0, animated: true)
|
||||
self.updateNavigationBarBlur(progress: active ? 1.0 : (1.0 - self.lastSearchExpansion))
|
||||
// Hide blur when search active (search bar moves up, no blur needed)
|
||||
self.updateNavigationBarBlur(progress: active ? 0.0 : (1.0 - self.lastSearchExpansion))
|
||||
self.navigationBlurView.isHidden = active
|
||||
self.animateToolbarForSearch(active: active)
|
||||
self.animateSearchBarPosition(active: active)
|
||||
if active {
|
||||
@@ -515,7 +518,7 @@ final class ChatListRootViewController: UIViewController, UINavigationController
|
||||
view.insertSubview(bg, belowSubview: searchHeaderView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
bg.topAnchor.constraint(equalTo: searchHeaderView.bottomAnchor, constant: searchBottomSpacing),
|
||||
bg.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
bg.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
bg.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
bg.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
@@ -531,12 +534,25 @@ final class ChatListRootViewController: UIViewController, UINavigationController
|
||||
hosting.didMove(toParent: self)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
hosting.view.topAnchor.constraint(equalTo: bg.topAnchor),
|
||||
// Content starts below search bar (not behind it)
|
||||
hosting.view.topAnchor.constraint(equalTo: searchHeaderView.bottomAnchor, constant: searchBottomSpacing),
|
||||
hosting.view.leadingAnchor.constraint(equalTo: bg.leadingAnchor),
|
||||
hosting.view.trailingAnchor.constraint(equalTo: bg.trailingAnchor),
|
||||
hosting.view.bottomAnchor.constraint(equalTo: bg.bottomAnchor),
|
||||
])
|
||||
|
||||
// Edge fade gradient below search bar (Telegram-style)
|
||||
let edgeFade = CAGradientLayer()
|
||||
edgeFade.colors = [
|
||||
UIColor(RosettaColors.Adaptive.background).cgColor,
|
||||
UIColor(RosettaColors.Adaptive.background).withAlphaComponent(0).cgColor,
|
||||
]
|
||||
edgeFade.locations = [0, 1]
|
||||
edgeFade.startPoint = CGPoint(x: 0.5, y: 0)
|
||||
edgeFade.endPoint = CGPoint(x: 0.5, y: 1)
|
||||
edgeFade.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: 14)
|
||||
hosting.view.layer.addSublayer(edgeFade)
|
||||
|
||||
searchOverlayView = bg
|
||||
searchContentHosting = hosting
|
||||
|
||||
@@ -1113,6 +1129,7 @@ private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
private(set) var isSearchActive = false
|
||||
|
||||
private let capsuleView = UIView()
|
||||
private let capsuleGlass = TelegramGlassUIView(frame: .zero)
|
||||
private let placeholderStack = UIStackView()
|
||||
private let placeholderIcon = UIImageView(image: UIImage(systemName: "magnifyingglass"))
|
||||
private let placeholderLabel = UILabel()
|
||||
@@ -1121,9 +1138,11 @@ private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
private let activeIcon = UIImageView(image: UIImage(systemName: "magnifyingglass"))
|
||||
private let textField = UITextField()
|
||||
private let inlineClearButton = UIButton(type: .system)
|
||||
private let cancelButton = UIButton(type: .system)
|
||||
private let cancelButton = UIButton(type: .custom)
|
||||
private let cancelGlass = TelegramGlassUIView(frame: .zero)
|
||||
|
||||
private var cancelWidthConstraint: NSLayoutConstraint!
|
||||
private var capsuleTrailingConstraint: NSLayoutConstraint!
|
||||
private var suppressQueryCallback = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
@@ -1142,6 +1161,17 @@ private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
applyColors()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
// Update glass frames and corner radii
|
||||
capsuleGlass.frame = capsuleView.bounds
|
||||
capsuleGlass.fixedCornerRadius = capsuleView.bounds.height * 0.5
|
||||
capsuleGlass.updateGlass()
|
||||
// cancelGlass uses constraints (sibling of cancelButton), just update glass
|
||||
cancelGlass.fixedCornerRadius = cancelButton.bounds.height * 0.5
|
||||
cancelGlass.updateGlass()
|
||||
}
|
||||
|
||||
func endSearch(animated: Bool, clearText: Bool) {
|
||||
setSearchActive(false, animated: animated, clearText: clearText)
|
||||
}
|
||||
@@ -1158,7 +1188,9 @@ private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
func updateExpansionProgress(_ progress: CGFloat) {
|
||||
let currentHeight = searchBarHeight * progress
|
||||
// Dynamic corner radius — Telegram: height * 0.5
|
||||
capsuleView.layer.cornerRadius = max(0, currentHeight * 0.5)
|
||||
let radius = max(0, currentHeight * 0.5)
|
||||
capsuleGlass.fixedCornerRadius = radius
|
||||
capsuleGlass.updateGlass()
|
||||
|
||||
// Telegram inner content alpha: 0 until 77%, then ramps to 1
|
||||
let innerAlpha = max(0.0, min(1.0, (progress - 0.77) / 0.23))
|
||||
@@ -1174,8 +1206,12 @@ private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
|
||||
capsuleView.translatesAutoresizingMaskIntoConstraints = false
|
||||
capsuleView.layer.cornerRadius = 22
|
||||
capsuleView.layer.borderWidth = 0
|
||||
capsuleView.clipsToBounds = true
|
||||
// Glass background for capsule (Telegram active search material)
|
||||
capsuleGlass.fixedCornerRadius = 22
|
||||
capsuleGlass.isHidden = true
|
||||
capsuleGlass.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
capsuleView.insertSubview(capsuleGlass, at: 0)
|
||||
addSubview(capsuleView)
|
||||
|
||||
placeholderStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -1216,13 +1252,20 @@ private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
activeStack.addArrangedSubview(inlineClearButton)
|
||||
capsuleView.addSubview(activeStack)
|
||||
|
||||
// Telegram-style circular X button (replaces "Cancel" text)
|
||||
// Glass background for X button (sibling, positioned behind)
|
||||
cancelGlass.translatesAutoresizingMaskIntoConstraints = false
|
||||
cancelGlass.fixedCornerRadius = 22
|
||||
cancelGlass.isUserInteractionEnabled = false
|
||||
cancelGlass.isHidden = true
|
||||
addSubview(cancelGlass)
|
||||
|
||||
// Telegram-style circular X button: 44pt, glass material
|
||||
cancelButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
let xConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .semibold)
|
||||
let xConfig = UIImage.SymbolConfiguration(pointSize: 16, weight: .medium)
|
||||
cancelButton.setImage(UIImage(systemName: "xmark", withConfiguration: xConfig), for: .normal)
|
||||
cancelButton.setTitle(nil, for: .normal)
|
||||
cancelButton.layer.cornerRadius = 18
|
||||
cancelButton.clipsToBounds = true
|
||||
cancelButton.clipsToBounds = false
|
||||
cancelButton.backgroundColor = .clear
|
||||
cancelButton.addTarget(self, action: #selector(handleCancelTapped), for: .touchUpInside)
|
||||
addSubview(cancelButton)
|
||||
|
||||
@@ -1230,38 +1273,45 @@ private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
capsuleView.addGestureRecognizer(tap)
|
||||
|
||||
cancelWidthConstraint = cancelButton.widthAnchor.constraint(equalToConstant: 0)
|
||||
// Fix 1: animate gap — 0 when inactive (symmetric), -8 when active
|
||||
let trailing = capsuleView.trailingAnchor.constraint(equalTo: cancelButton.leadingAnchor, constant: 0)
|
||||
capsuleTrailingConstraint = trailing
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
capsuleView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
capsuleView.topAnchor.constraint(equalTo: topAnchor),
|
||||
capsuleView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
capsuleView.trailingAnchor.constraint(equalTo: cancelButton.leadingAnchor, constant: -8),
|
||||
trailing,
|
||||
|
||||
cancelButton.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
cancelButton.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
cancelButton.heightAnchor.constraint(equalToConstant: 36),
|
||||
cancelButton.heightAnchor.constraint(equalToConstant: 44),
|
||||
cancelWidthConstraint,
|
||||
|
||||
// Glass behind X button (same frame)
|
||||
cancelGlass.topAnchor.constraint(equalTo: cancelButton.topAnchor),
|
||||
cancelGlass.leadingAnchor.constraint(equalTo: cancelButton.leadingAnchor),
|
||||
cancelGlass.trailingAnchor.constraint(equalTo: cancelButton.trailingAnchor),
|
||||
cancelGlass.bottomAnchor.constraint(equalTo: cancelButton.bottomAnchor),
|
||||
|
||||
placeholderStack.centerXAnchor.constraint(equalTo: capsuleView.centerXAnchor),
|
||||
placeholderStack.centerYAnchor.constraint(equalTo: capsuleView.centerYAnchor),
|
||||
|
||||
activeStack.leadingAnchor.constraint(equalTo: capsuleView.leadingAnchor, constant: 12),
|
||||
// Fix 4: leading 8pt (Telegram parity)
|
||||
activeStack.leadingAnchor.constraint(equalTo: capsuleView.leadingAnchor, constant: 8),
|
||||
activeStack.trailingAnchor.constraint(equalTo: capsuleView.trailingAnchor, constant: -10),
|
||||
activeStack.topAnchor.constraint(equalTo: capsuleView.topAnchor),
|
||||
activeStack.bottomAnchor.constraint(equalTo: capsuleView.bottomAnchor),
|
||||
|
||||
// Fix 2: fixed width prevents stretching to 44pt height
|
||||
activeIcon.widthAnchor.constraint(equalToConstant: 20),
|
||||
inlineClearButton.widthAnchor.constraint(equalToConstant: 24),
|
||||
inlineClearButton.heightAnchor.constraint(equalToConstant: 24),
|
||||
])
|
||||
}
|
||||
|
||||
private func applyColors() {
|
||||
// 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)
|
||||
@@ -1272,19 +1322,44 @@ private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
// inputTextColor: dark #ffffff, light #000000
|
||||
textField.textColor = isDark ? .white : .black
|
||||
inlineClearButton.tintColor = placeholderColor
|
||||
// Telegram X button: dark circle background, white icon
|
||||
cancelButton.backgroundColor = isDark
|
||||
? UIColor(red: 0x2c/255.0, green: 0x2c/255.0, blue: 0x2e/255.0, alpha: 1.0)
|
||||
: UIColor(red: 0xd1/255.0, green: 0xd1/255.0, blue: 0xd6/255.0, alpha: 1.0)
|
||||
// X button icon: white on dark, black on light
|
||||
cancelButton.tintColor = isDark ? .white : .black
|
||||
// Apply correct background for current state
|
||||
applyCapsuleBackground()
|
||||
}
|
||||
|
||||
/// Inactive: solid #272728. Active: TelegramGlassUIView (glass material).
|
||||
private func applyCapsuleBackground() {
|
||||
let isDark = traitCollection.userInterfaceStyle == .dark
|
||||
let solidFill = 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)
|
||||
|
||||
if isSearchActive {
|
||||
// Glass material (Telegram active search)
|
||||
capsuleView.backgroundColor = .clear
|
||||
capsuleGlass.isHidden = false
|
||||
capsuleGlass.updateGlass()
|
||||
cancelGlass.isHidden = false
|
||||
cancelGlass.updateGlass()
|
||||
} else {
|
||||
// Solid color (Telegram inactive search)
|
||||
capsuleView.backgroundColor = solidFill
|
||||
capsuleGlass.isHidden = true
|
||||
cancelGlass.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
private func updateVisualState(animated: Bool) {
|
||||
// Switch capsule background: solid ↔ glass
|
||||
applyCapsuleBackground()
|
||||
|
||||
let updates = {
|
||||
self.placeholderStack.alpha = self.isSearchActive ? 0 : 1
|
||||
self.activeStack.alpha = self.isSearchActive ? 1 : 0
|
||||
self.cancelButton.alpha = self.isSearchActive ? 1 : 0
|
||||
self.cancelWidthConstraint.constant = self.isSearchActive ? 36 : 0
|
||||
self.cancelWidthConstraint.constant = self.isSearchActive ? 44 : 0
|
||||
self.capsuleTrailingConstraint.constant = self.isSearchActive ? -8 : 0
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user