Исправление расширения поля пароля при переключении видимости: перенос toggle в UIKit

This commit is contained in:
2026-03-12 04:01:21 +05:00
parent dc8e179c10
commit 70deaaf7f7
23 changed files with 706 additions and 285 deletions

View File

@@ -140,6 +140,11 @@ private extension ChatListSearchContent {
ForEach(viewModel.recentSearches, id: \.publicKey) { recent in
recentRow(recent)
if recent.publicKey != viewModel.recentSearches.last?.publicKey {
Divider()
.padding(.leading, 68)
.foregroundStyle(RosettaColors.Adaptive.divider)
}
}
}
@@ -179,7 +184,7 @@ private extension ChatListSearchContent {
let colorIdx = RosettaColors.avatarColorIndex(for: user.title, publicKey: user.publicKey)
return Button {
onSelectRecent(user.username.isEmpty ? user.publicKey : user.username)
onOpenDialog(ChatRoute(recent: user))
} label: {
HStack(spacing: 10) {
AvatarView(
@@ -207,6 +212,7 @@ private extension ChatListSearchContent {
}
.padding(.horizontal, 16)
.padding(.vertical, 5)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
@@ -263,6 +269,7 @@ private extension ChatListSearchContent {
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}

View File

@@ -32,16 +32,13 @@ struct ChatListView: View {
@State private var hasPinnedChats = false
@FocusState private var isSearchFocused: Bool
@MainActor static var _bodyCount = 0
var body: some View {
let _ = Self._bodyCount += 1
let _ = print("🟡 ChatListView.body #\(Self._bodyCount)")
NavigationStack(path: $navigationState.path) {
VStack(spacing: 0) {
// Custom search bar
customSearchBar
.padding(.horizontal, 16)
.padding(.top, 6)
.padding(.top, 12)
.padding(.bottom, 8)
.background(
(hasPinnedChats && !isSearchActive
@@ -290,7 +287,7 @@ private extension ChatListView {
Text("Edit")
.font(.system(size: 17, weight: .medium))
.foregroundStyle(RosettaColors.Adaptive.text)
.frame(height: 44)
.frame(height: 40)
.padding(.horizontal, 10)
}
.buttonStyle(.plain)
@@ -312,7 +309,7 @@ private extension ChatListView {
.resizable()
.scaledToFit()
.frame(width: 22, height: 22)
.frame(width: 44, height: 44)
.frame(width: 40, height: 40)
}
.buttonStyle(.plain)
.accessibilityLabel("Add chat")
@@ -323,7 +320,7 @@ private extension ChatListView {
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.frame(width: 44, height: 44)
.frame(width: 40, height: 40)
}
.buttonStyle(.plain)
.accessibilityLabel("New chat")
@@ -377,10 +374,7 @@ private struct ToolbarTitleView: View {
/// Changes to these `@Observable` singletons only re-render this small view,
/// not the parent ChatListView / NavigationStack.
private struct ToolbarStoriesAvatar: View {
@MainActor static var _bodyCount = 0
var body: some View {
let _ = Self._bodyCount += 1
let _ = print("🟣 ToolbarStoriesAvatar.body #\(Self._bodyCount)")
let pk = AccountManager.shared.currentAccount?.publicKey ?? ""
let initials = RosettaColors.initials(
name: SessionManager.shared.displayName, publicKey: pk
@@ -396,10 +390,7 @@ private struct ToolbarStoriesAvatar: View {
/// During handshake, `connectionState` changes 4+ times rapidly this view
/// absorbs those re-renders instead of cascading them to the NavigationStack.
private struct DeviceVerificationBannersContainer: View {
@MainActor static var _bodyCount = 0
var body: some View {
let _ = Self._bodyCount += 1
let _ = print("⚪ DeviceVerificationBanners.body #\(Self._bodyCount)")
let proto = ProtocolManager.shared
if proto.connectionState == .deviceVerificationRequired {
@@ -424,11 +415,7 @@ private struct ChatListDialogContent: View {
@ObservedObject var viewModel: ChatListViewModel
@ObservedObject var navigationState: ChatListNavigationState
var onPinnedStateChange: (Bool) -> Void = { _ in }
@MainActor static var _bodyCount = 0
var body: some View {
let _ = Self._bodyCount += 1
let _ = print("🔶 ChatListDialogContent.body #\(Self._bodyCount)")
let hasPinned = !viewModel.pinnedDialogs.isEmpty
if viewModel.filteredDialogs.isEmpty && !viewModel.isLoading {
ChatEmptyStateView(searchText: "")

View File

@@ -46,8 +46,8 @@ final class ChatListViewModel: ObservableObject {
var unpinnedDialogs: [Dialog] { filteredDialogs.filter { !$0.isPinned } }
var totalUnreadCount: Int {
DialogRepository.shared.sortedDialogs
.filter { !$0.isMuted }
DialogRepository.shared.dialogs.values
.lazy.filter { !$0.isMuted }
.reduce(0) { $0 + $1.unreadCount }
}
@@ -172,7 +172,7 @@ final class ChatListViewModel: ObservableObject {
var packet = PacketSearch()
packet.privateKey = hash
packet.search = query
packet.search = query.lowercased()
Self.logger.debug("📤 Sending search packet for '\(query)'")
ProtocolManager.shared.sendPacket(packet)
}

View File

@@ -1,4 +1,5 @@
import SwiftUI
import Combine
// MARK: - ChatRowView
@@ -20,6 +21,11 @@ import SwiftUI
struct ChatRowView: View {
let dialog: Dialog
/// Desktop parity: recheck delivery timeout every 40s so clock error
/// transitions happen automatically without user scrolling.
@State private var now = Date()
private let recheckTimer = Timer.publish(every: 40, on: .main, in: .common).autoconnect()
var displayTitle: String {
if dialog.isSavedMessages { return "Saved Messages" }
if !dialog.opponentTitle.isEmpty { return dialog.opponentTitle }
@@ -38,6 +44,7 @@ struct ChatRowView: View {
.padding(.trailing, 16)
.frame(height: 78)
.contentShape(Rectangle())
.onReceive(recheckTimer) { now = $0 }
}
}
@@ -215,7 +222,7 @@ private extension ChatRowView {
private var isWithinWaitingWindow: Bool {
guard dialog.lastMessageTimestamp > 0 else { return true }
let sentDate = Date(timeIntervalSince1970: Double(dialog.lastMessageTimestamp) / 1000)
return Date().timeIntervalSince(sentDate) < Self.maxWaitingSeconds
return now.timeIntervalSince(sentDate) < Self.maxWaitingSeconds
}
var unreadBadge: some View {
@@ -244,6 +251,16 @@ private extension ChatRowView {
// MARK: - Time Formatting
private extension ChatRowView {
private static let timeFormatter: DateFormatter = {
let f = DateFormatter(); f.dateFormat = "h:mm a"; return f
}()
private static let dayFormatter: DateFormatter = {
let f = DateFormatter(); f.dateFormat = "EEE"; return f
}()
private static let dateFormatter: DateFormatter = {
let f = DateFormatter(); f.dateFormat = "dd.MM.yy"; return f
}()
var formattedTime: String {
guard dialog.lastMessageTimestamp > 0 else { return "" }
@@ -252,19 +269,13 @@ private extension ChatRowView {
let calendar = Calendar.current
if calendar.isDateInToday(date) {
let f = DateFormatter()
f.dateFormat = "h:mm a"
return f.string(from: date)
return Self.timeFormatter.string(from: date)
} else if calendar.isDateInYesterday(date) {
return "Yesterday"
} else if let days = calendar.dateComponents([.day], from: date, to: now).day, days < 7 {
let f = DateFormatter()
f.dateFormat = "EEE"
return f.string(from: date)
return Self.dayFormatter.string(from: date)
} else {
let f = DateFormatter()
f.dateFormat = "dd.MM.yy"
return f.string(from: date)
return Self.dateFormatter.string(from: date)
}
}
}