Фикс: инсет маски шиммера скелетона + смешанные бабблы в групповом скелетоне
This commit is contained in:
@@ -1071,6 +1071,7 @@ private final class ChatDetailTitlePill: UIControl {
|
|||||||
private let glassView = TelegramGlassUIView(frame: .zero)
|
private let glassView = TelegramGlassUIView(frame: .zero)
|
||||||
private let titleLabel = UILabel()
|
private let titleLabel = UILabel()
|
||||||
private let subtitleLabel = UILabel()
|
private let subtitleLabel = UILabel()
|
||||||
|
private let typingDotsView = TypingDotsView()
|
||||||
|
|
||||||
private let route: ChatRoute
|
private let route: ChatRoute
|
||||||
private weak var viewModel: ChatDetailViewModel?
|
private weak var viewModel: ChatDetailViewModel?
|
||||||
@@ -1102,6 +1103,10 @@ private final class ChatDetailTitlePill: UIControl {
|
|||||||
subtitleLabel.textAlignment = .center
|
subtitleLabel.textAlignment = .center
|
||||||
subtitleLabel.lineBreakMode = .byTruncatingTail
|
subtitleLabel.lineBreakMode = .byTruncatingTail
|
||||||
addSubview(subtitleLabel)
|
addSubview(subtitleLabel)
|
||||||
|
|
||||||
|
typingDotsView.isUserInteractionEnabled = false
|
||||||
|
typingDotsView.isHidden = true
|
||||||
|
addSubview(typingDotsView)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func observeChanges() {
|
private func observeChanges() {
|
||||||
@@ -1167,13 +1172,27 @@ private final class ChatDetailTitlePill: UIControl {
|
|||||||
}
|
}
|
||||||
subtitleLabel.text = subtitle
|
subtitleLabel.text = subtitle
|
||||||
subtitleLabel.textColor = subtitleColor
|
subtitleLabel.textColor = subtitleColor
|
||||||
|
|
||||||
|
// Show/hide typing dots animation
|
||||||
|
let isTypingActive = (viewModel?.isTyping == true)
|
||||||
|
|| !(viewModel?.typingSenderNames ?? []).isEmpty
|
||||||
|
if isTypingActive {
|
||||||
|
typingDotsView.dotColor = subtitleColor
|
||||||
|
typingDotsView.isHidden = false
|
||||||
|
typingDotsView.startAnimating()
|
||||||
|
} else {
|
||||||
|
typingDotsView.isHidden = true
|
||||||
|
typingDotsView.stopAnimating()
|
||||||
|
}
|
||||||
|
setNeedsLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the natural content width (max of title/subtitle label widths).
|
/// Returns the natural content width (max of title/subtitle label widths).
|
||||||
func contentWidth() -> CGFloat {
|
func contentWidth() -> CGFloat {
|
||||||
let titleW = titleLabel.intrinsicContentSize.width
|
let titleW = titleLabel.intrinsicContentSize.width
|
||||||
let subtitleW = subtitleLabel.intrinsicContentSize.width
|
let subtitleW = subtitleLabel.intrinsicContentSize.width
|
||||||
return max(titleW, subtitleW)
|
let dotsExtra: CGFloat = typingDotsView.isHidden ? 0 : 24
|
||||||
|
return max(titleW, subtitleW + dotsExtra)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
@@ -1195,7 +1214,24 @@ private final class ChatDetailTitlePill: UIControl {
|
|||||||
let totalH = titleH + spacing + subtitleH
|
let totalH = titleH + spacing + subtitleH
|
||||||
let topY = (bounds.height - totalH) / 2
|
let topY = (bounds.height - totalH) / 2
|
||||||
titleLabel.frame = CGRect(x: hPad, y: topY, width: contentW, height: titleH)
|
titleLabel.frame = CGRect(x: hPad, y: topY, width: contentW, height: titleH)
|
||||||
subtitleLabel.frame = CGRect(x: hPad, y: topY + titleH + spacing, width: contentW, height: subtitleH)
|
|
||||||
|
let subtitleY = topY + titleH + spacing
|
||||||
|
if !typingDotsView.isHidden {
|
||||||
|
// Dots (24×14) + subtitle text, centered horizontally
|
||||||
|
let dotsW: CGFloat = 24
|
||||||
|
let dotsH: CGFloat = 14
|
||||||
|
let textW = subtitleLabel.intrinsicContentSize.width
|
||||||
|
let totalW = dotsW + textW
|
||||||
|
let startX = (bounds.width - totalW) / 2
|
||||||
|
|
||||||
|
typingDotsView.frame = CGRect(x: startX, y: subtitleY + (subtitleH - dotsH) / 2 + 1, width: dotsW, height: dotsH)
|
||||||
|
subtitleLabel.frame = CGRect(x: startX + dotsW, y: subtitleY, width: textW, height: subtitleH)
|
||||||
|
subtitleLabel.textAlignment = .left
|
||||||
|
} else {
|
||||||
|
typingDotsView.frame = .zero
|
||||||
|
subtitleLabel.frame = CGRect(x: hPad, y: subtitleY, width: contentW, height: subtitleH)
|
||||||
|
subtitleLabel.textAlignment = .center
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
titleLabel.frame = CGRect(x: hPad, y: 0, width: contentW, height: bounds.height)
|
titleLabel.frame = CGRect(x: hPad, y: 0, width: contentW, height: bounds.height)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,22 +53,22 @@ final class NativeSkeletonView: UIView {
|
|||||||
BubbleSpec(widthFrac: 0.50, height: 44, outgoing: false),
|
BubbleSpec(widthFrac: 0.50, height: 44, outgoing: false),
|
||||||
]
|
]
|
||||||
|
|
||||||
// Groups: all incoming (Telegram parity)
|
// Groups: mix of incoming/outgoing (like a real group conversation)
|
||||||
private static let groupSpecs: [BubbleSpec] = [
|
private static let groupSpecs: [BubbleSpec] = [
|
||||||
BubbleSpec(widthFrac: 0.47, height: 44, outgoing: false),
|
BubbleSpec(widthFrac: 0.47, height: 44, outgoing: false),
|
||||||
BubbleSpec(widthFrac: 0.58, height: 44, outgoing: false),
|
BubbleSpec(widthFrac: 0.58, height: 44, outgoing: false),
|
||||||
|
BubbleSpec(widthFrac: 0.50, height: 44, outgoing: true),
|
||||||
BubbleSpec(widthFrac: 0.69, height: 60, outgoing: false),
|
BubbleSpec(widthFrac: 0.69, height: 60, outgoing: false),
|
||||||
|
BubbleSpec(widthFrac: 0.45, height: 44, outgoing: true),
|
||||||
BubbleSpec(widthFrac: 0.47, height: 44, outgoing: false),
|
BubbleSpec(widthFrac: 0.47, height: 44, outgoing: false),
|
||||||
BubbleSpec(widthFrac: 0.58, height: 44, outgoing: false),
|
BubbleSpec(widthFrac: 0.58, height: 44, outgoing: false),
|
||||||
|
BubbleSpec(widthFrac: 0.55, height: 44, outgoing: true),
|
||||||
BubbleSpec(widthFrac: 0.36, height: 60, outgoing: false),
|
BubbleSpec(widthFrac: 0.36, height: 60, outgoing: false),
|
||||||
BubbleSpec(widthFrac: 0.47, height: 44, outgoing: false),
|
BubbleSpec(widthFrac: 0.47, height: 44, outgoing: false),
|
||||||
BubbleSpec(widthFrac: 0.36, height: 44, outgoing: false),
|
BubbleSpec(widthFrac: 0.40, height: 44, outgoing: true),
|
||||||
BubbleSpec(widthFrac: 0.58, height: 44, outgoing: false),
|
BubbleSpec(widthFrac: 0.58, height: 44, outgoing: false),
|
||||||
BubbleSpec(widthFrac: 0.69, height: 60, outgoing: false),
|
BubbleSpec(widthFrac: 0.69, height: 60, outgoing: false),
|
||||||
BubbleSpec(widthFrac: 0.58, height: 44, outgoing: false),
|
BubbleSpec(widthFrac: 0.50, height: 44, outgoing: true),
|
||||||
BubbleSpec(widthFrac: 0.36, height: 44, outgoing: false),
|
|
||||||
BubbleSpec(widthFrac: 0.47, height: 44, outgoing: false),
|
|
||||||
BubbleSpec(widthFrac: 0.58, height: 44, outgoing: false),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
private static let avatarSize: CGFloat = 38
|
private static let avatarSize: CGFloat = 38
|
||||||
@@ -99,6 +99,7 @@ final class NativeSkeletonView: UIView {
|
|||||||
private let containerView = UIView()
|
private let containerView = UIView()
|
||||||
private var bubbleImageViews: [UIImageView] = []
|
private var bubbleImageViews: [UIImageView] = []
|
||||||
private var avatarViews: [UIView] = []
|
private var avatarViews: [UIView] = []
|
||||||
|
private var shimmerLayer: CALayer?
|
||||||
private var shimmerGradients: [CAGradientLayer] = []
|
private var shimmerGradients: [CAGradientLayer] = []
|
||||||
private var isShimmerRunning = false
|
private var isShimmerRunning = false
|
||||||
|
|
||||||
@@ -184,9 +185,12 @@ final class NativeSkeletonView: UIView {
|
|||||||
bubbleImageViews.append(iv)
|
bubbleImageViews.append(iv)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shimmer mask: use actual bubble path shape (not rect)
|
// Shimmer mask: inset path by 1pt so shimmer stays inside the raster bubble image
|
||||||
|
// (vector BezierPath vs 9-slice raster image have slightly different anti-aliased edges)
|
||||||
|
let maskInset: CGFloat = 1.0
|
||||||
|
let maskRect = CGRect(origin: .zero, size: bubbleFrame.size).insetBy(dx: maskInset, dy: maskInset)
|
||||||
let bubblePath = BubbleGeometryEngine.makeBezierPath(
|
let bubblePath = BubbleGeometryEngine.makeBezierPath(
|
||||||
in: CGRect(origin: .zero, size: bubbleFrame.size),
|
in: maskRect,
|
||||||
mergeType: .none,
|
mergeType: .none,
|
||||||
outgoing: spec.outgoing,
|
outgoing: spec.outgoing,
|
||||||
metrics: metrics
|
metrics: metrics
|
||||||
|
|||||||
@@ -194,14 +194,15 @@ private extension ChatListSearchContent {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Desktop parity: search subtitle shows @username, not online/offline.
|
// Telegram parity: subtitle shows online status, not @username
|
||||||
if !isSelf {
|
if !isSelf {
|
||||||
Text(user.username.isEmpty
|
let dialog = DialogRepository.shared.dialogs[user.publicKey]
|
||||||
? "@\(String(user.publicKey.prefix(10)))..."
|
let isOnline = dialog?.isOnline ?? false
|
||||||
: "@\(user.username)"
|
Text(isOnline ? "online" : "last seen recently")
|
||||||
)
|
|
||||||
.font(.system(size: 13))
|
.font(.system(size: 13))
|
||||||
.foregroundStyle(RosettaColors.Adaptive.textSecondary)
|
.foregroundStyle(isOnline
|
||||||
|
? RosettaColors.primaryBlue
|
||||||
|
: RosettaColors.Adaptive.textSecondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ final class ChatListRootViewController: UIViewController, UINavigationController
|
|||||||
private var searchResultUsersByKey: [String: SearchUser] = [:]
|
private var searchResultUsersByKey: [String: SearchUser] = [:]
|
||||||
private let searchTopSpacing: CGFloat = 10
|
private let searchTopSpacing: CGFloat = 10
|
||||||
private let searchBottomSpacing: CGFloat = 5
|
private let searchBottomSpacing: CGFloat = 5
|
||||||
|
|
||||||
|
// Search overlay (shown when search is active)
|
||||||
|
private var searchOverlayView: UIView?
|
||||||
|
private var searchContentHosting: UIHostingController<AnyView>?
|
||||||
private let searchHeaderHeight: CGFloat = 44
|
private let searchHeaderHeight: CGFloat = 44
|
||||||
private var searchChromeHeight: CGFloat {
|
private var searchChromeHeight: CGFloat {
|
||||||
searchTopSpacing + searchHeaderHeight + searchBottomSpacing
|
searchTopSpacing + searchHeaderHeight + searchBottomSpacing
|
||||||
@@ -218,6 +222,9 @@ final class ChatListRootViewController: UIViewController, UINavigationController
|
|||||||
editButtonControl.isUserInteractionEnabled = true
|
editButtonControl.isUserInteractionEnabled = true
|
||||||
rightButtonsControl.isUserInteractionEnabled = true
|
rightButtonsControl.isUserInteractionEnabled = true
|
||||||
toolbarTitleView.isUserInteractionEnabled = true
|
toolbarTitleView.isUserInteractionEnabled = true
|
||||||
|
// Restore search bar position and tear down overlay
|
||||||
|
searchHeaderTopConstraint?.constant = headerBarHeight + searchTopSpacing
|
||||||
|
teardownSearchOverlayImmediately()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,6 +302,7 @@ final class ChatListRootViewController: UIViewController, UINavigationController
|
|||||||
self.currentSearchQuery = query
|
self.currentSearchQuery = query
|
||||||
self.viewModel.setSearchQuery(query)
|
self.viewModel.setSearchQuery(query)
|
||||||
self.renderList()
|
self.renderList()
|
||||||
|
self.updateSearchOverlayContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
searchHeaderView.onActiveChanged = { [weak self] active in
|
searchHeaderView.onActiveChanged = { [weak self] active in
|
||||||
@@ -302,6 +310,12 @@ final class ChatListRootViewController: UIViewController, UINavigationController
|
|||||||
self.applySearchExpansion(1.0, animated: true)
|
self.applySearchExpansion(1.0, animated: true)
|
||||||
self.updateNavigationBarBlur(progress: active ? 1.0 : (1.0 - self.lastSearchExpansion))
|
self.updateNavigationBarBlur(progress: active ? 1.0 : (1.0 - self.lastSearchExpansion))
|
||||||
self.animateToolbarForSearch(active: active)
|
self.animateToolbarForSearch(active: active)
|
||||||
|
self.animateSearchBarPosition(active: active)
|
||||||
|
if active {
|
||||||
|
self.showSearchOverlay()
|
||||||
|
} else {
|
||||||
|
self.hideSearchOverlay()
|
||||||
|
}
|
||||||
self.onSearchActiveChanged?(active)
|
self.onSearchActiveChanged?(active)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -469,6 +483,119 @@ final class ChatListRootViewController: UIViewController, UINavigationController
|
|||||||
toolbarTitleView.isUserInteractionEnabled = !active
|
toolbarTitleView.isUserInteractionEnabled = !active
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Search Bar Position Animation
|
||||||
|
|
||||||
|
/// Telegram: search bar moves to just below status bar when active
|
||||||
|
private func animateSearchBarPosition(active: Bool) {
|
||||||
|
let targetTop: CGFloat = active
|
||||||
|
? searchTopSpacing // just below status bar
|
||||||
|
: headerBarHeight + searchTopSpacing // below custom toolbar
|
||||||
|
searchHeaderTopConstraint?.constant = targetTop
|
||||||
|
|
||||||
|
UIView.animate(
|
||||||
|
withDuration: 0.5,
|
||||||
|
delay: 0,
|
||||||
|
usingSpringWithDamping: 0.78,
|
||||||
|
initialSpringVelocity: 0,
|
||||||
|
options: [.beginFromCurrentState],
|
||||||
|
animations: { self.view.layoutIfNeeded() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Search Overlay
|
||||||
|
|
||||||
|
private func showSearchOverlay() {
|
||||||
|
guard searchOverlayView == nil else { return }
|
||||||
|
|
||||||
|
let bg = UIView()
|
||||||
|
bg.backgroundColor = UIColor(RosettaColors.Adaptive.background)
|
||||||
|
bg.alpha = 0
|
||||||
|
bg.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
bg.layer.zPosition = 45
|
||||||
|
view.insertSubview(bg, belowSubview: searchHeaderView)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
bg.topAnchor.constraint(equalTo: searchHeaderView.bottomAnchor, constant: searchBottomSpacing),
|
||||||
|
bg.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
bg.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
bg.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
let content = makeSearchContentView()
|
||||||
|
let hosting = UIHostingController(rootView: AnyView(content))
|
||||||
|
hosting.view.backgroundColor = .clear
|
||||||
|
hosting.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
addChild(hosting)
|
||||||
|
bg.addSubview(hosting.view)
|
||||||
|
hosting.didMove(toParent: self)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
hosting.view.topAnchor.constraint(equalTo: bg.topAnchor),
|
||||||
|
hosting.view.leadingAnchor.constraint(equalTo: bg.leadingAnchor),
|
||||||
|
hosting.view.trailingAnchor.constraint(equalTo: bg.trailingAnchor),
|
||||||
|
hosting.view.bottomAnchor.constraint(equalTo: bg.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
searchOverlayView = bg
|
||||||
|
searchContentHosting = hosting
|
||||||
|
|
||||||
|
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut) {
|
||||||
|
bg.alpha = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hideSearchOverlay() {
|
||||||
|
guard let overlay = searchOverlayView else { return }
|
||||||
|
|
||||||
|
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut, animations: {
|
||||||
|
overlay.alpha = 0
|
||||||
|
}) { [weak self] _ in
|
||||||
|
guard let self else { return }
|
||||||
|
self.searchContentHosting?.willMove(toParent: nil)
|
||||||
|
self.searchContentHosting?.view.removeFromSuperview()
|
||||||
|
self.searchContentHosting?.removeFromParent()
|
||||||
|
self.searchContentHosting = nil
|
||||||
|
overlay.removeFromSuperview()
|
||||||
|
self.searchOverlayView = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateSearchOverlayContent() {
|
||||||
|
guard let hosting = searchContentHosting else { return }
|
||||||
|
hosting.rootView = AnyView(makeSearchContentView())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeSearchContentView() -> some View {
|
||||||
|
ChatListSearchContent(
|
||||||
|
searchText: currentSearchQuery,
|
||||||
|
viewModel: viewModel,
|
||||||
|
onSelectRecent: { [weak self] query in
|
||||||
|
guard let self else { return }
|
||||||
|
self.searchHeaderView.setQueryFromOutside(query)
|
||||||
|
},
|
||||||
|
onOpenDialog: { [weak self] route in
|
||||||
|
guard let self else { return }
|
||||||
|
self.searchHeaderView.endSearch(animated: false, clearText: true)
|
||||||
|
self.animateSearchBarPosition(active: false)
|
||||||
|
self.hideSearchOverlay()
|
||||||
|
self.animateToolbarForSearch(active: false)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in
|
||||||
|
self?.openChat(route: route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func teardownSearchOverlayImmediately() {
|
||||||
|
searchContentHosting?.willMove(toParent: nil)
|
||||||
|
searchContentHosting?.view.removeFromSuperview()
|
||||||
|
searchContentHosting?.removeFromParent()
|
||||||
|
searchContentHosting = nil
|
||||||
|
searchOverlayView?.removeFromSuperview()
|
||||||
|
searchOverlayView = nil
|
||||||
|
}
|
||||||
|
|
||||||
private func applySearchExpansion(_ expansion: CGFloat, animated: Bool) {
|
private func applySearchExpansion(_ expansion: CGFloat, animated: Bool) {
|
||||||
let clamped = max(0.0, min(1.0, expansion))
|
let clamped = max(0.0, min(1.0, expansion))
|
||||||
if searchHeaderView.isSearchActive && clamped < 0.999 {
|
if searchHeaderView.isSearchActive && clamped < 0.999 {
|
||||||
@@ -1019,6 +1146,11 @@ private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate {
|
|||||||
setSearchActive(false, animated: animated, clearText: clearText)
|
setSearchActive(false, animated: animated, clearText: clearText)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Called from search overlay when user selects a recent search query
|
||||||
|
func setQueryFromOutside(_ text: String) {
|
||||||
|
setQueryText(text)
|
||||||
|
}
|
||||||
|
|
||||||
/// Telegram-style expansion animation:
|
/// Telegram-style expansion animation:
|
||||||
/// - Corner radius scales with height (pill shape maintained)
|
/// - Corner radius scales with height (pill shape maintained)
|
||||||
/// - Text/icon alpha: invisible until 77% expanded, then ramps to 1.0
|
/// - Text/icon alpha: invisible until 77% expanded, then ramps to 1.0
|
||||||
@@ -1084,10 +1216,13 @@ private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate {
|
|||||||
activeStack.addArrangedSubview(inlineClearButton)
|
activeStack.addArrangedSubview(inlineClearButton)
|
||||||
capsuleView.addSubview(activeStack)
|
capsuleView.addSubview(activeStack)
|
||||||
|
|
||||||
|
// Telegram-style circular X button (replaces "Cancel" text)
|
||||||
cancelButton.translatesAutoresizingMaskIntoConstraints = false
|
cancelButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
cancelButton.setTitle("Cancel", for: .normal)
|
let xConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .semibold)
|
||||||
cancelButton.titleLabel?.font = .systemFont(ofSize: 17)
|
cancelButton.setImage(UIImage(systemName: "xmark", withConfiguration: xConfig), for: .normal)
|
||||||
cancelButton.contentHorizontalAlignment = .right
|
cancelButton.setTitle(nil, for: .normal)
|
||||||
|
cancelButton.layer.cornerRadius = 18
|
||||||
|
cancelButton.clipsToBounds = true
|
||||||
cancelButton.addTarget(self, action: #selector(handleCancelTapped), for: .touchUpInside)
|
cancelButton.addTarget(self, action: #selector(handleCancelTapped), for: .touchUpInside)
|
||||||
addSubview(cancelButton)
|
addSubview(cancelButton)
|
||||||
|
|
||||||
@@ -1100,10 +1235,11 @@ private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate {
|
|||||||
capsuleView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
capsuleView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
capsuleView.topAnchor.constraint(equalTo: topAnchor),
|
capsuleView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
capsuleView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
capsuleView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
capsuleView.trailingAnchor.constraint(equalTo: cancelButton.leadingAnchor),
|
capsuleView.trailingAnchor.constraint(equalTo: cancelButton.leadingAnchor, constant: -8),
|
||||||
|
|
||||||
cancelButton.trailingAnchor.constraint(equalTo: trailingAnchor),
|
cancelButton.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
cancelButton.centerYAnchor.constraint(equalTo: centerYAnchor),
|
cancelButton.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
|
cancelButton.heightAnchor.constraint(equalToConstant: 36),
|
||||||
cancelWidthConstraint,
|
cancelWidthConstraint,
|
||||||
|
|
||||||
placeholderStack.centerXAnchor.constraint(equalTo: capsuleView.centerXAnchor),
|
placeholderStack.centerXAnchor.constraint(equalTo: capsuleView.centerXAnchor),
|
||||||
@@ -1136,7 +1272,11 @@ private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate {
|
|||||||
// inputTextColor: dark #ffffff, light #000000
|
// inputTextColor: dark #ffffff, light #000000
|
||||||
textField.textColor = isDark ? .white : .black
|
textField.textColor = isDark ? .white : .black
|
||||||
inlineClearButton.tintColor = placeholderColor
|
inlineClearButton.tintColor = placeholderColor
|
||||||
cancelButton.setTitleColor(UIColor(RosettaColors.primaryBlue), for: .normal)
|
// 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)
|
||||||
|
cancelButton.tintColor = isDark ? .white : .black
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateVisualState(animated: Bool) {
|
private func updateVisualState(animated: Bool) {
|
||||||
@@ -1144,7 +1284,7 @@ private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate {
|
|||||||
self.placeholderStack.alpha = self.isSearchActive ? 0 : 1
|
self.placeholderStack.alpha = self.isSearchActive ? 0 : 1
|
||||||
self.activeStack.alpha = self.isSearchActive ? 1 : 0
|
self.activeStack.alpha = self.isSearchActive ? 1 : 0
|
||||||
self.cancelButton.alpha = self.isSearchActive ? 1 : 0
|
self.cancelButton.alpha = self.isSearchActive ? 1 : 0
|
||||||
self.cancelWidthConstraint.constant = self.isSearchActive ? 64 : 0
|
self.cancelWidthConstraint.constant = self.isSearchActive ? 36 : 0
|
||||||
self.layoutIfNeeded()
|
self.layoutIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user