Исправления UI: центрирование Saved Messages, размеры тулбара звонков, отображение "Connecting...", локальная отправка в Saved Messages

This commit is contained in:
2026-03-11 01:41:40 +05:00
parent 0f5094df10
commit fa003e9edb
15 changed files with 913 additions and 155 deletions

View File

@@ -45,6 +45,7 @@ struct ChatDetailView: View {
private var subtitleText: String {
if route.isSavedMessages { return "" }
if ProtocolManager.shared.connectionState != .authenticated { return "connecting..." }
if isTyping { return "typing..." }
if let dialog, dialog.isOnline { return "online" }
return "offline"
@@ -168,14 +169,16 @@ private extension ChatDetailView {
}
}
Text(subtitleText)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(
isTyping || (dialog?.isOnline == true)
? RosettaColors.online
: RosettaColors.Adaptive.textSecondary
)
.lineLimit(1)
if !subtitleText.isEmpty {
Text(subtitleText)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(
isTyping || (dialog?.isOnline == true)
? RosettaColors.online
: RosettaColors.Adaptive.textSecondary
)
.lineLimit(1)
}
}
.padding(.horizontal, 12)
.frame(height: 44)
@@ -218,14 +221,16 @@ private extension ChatDetailView {
}
}
Text(subtitleText)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(
isTyping || (dialog?.isOnline == true)
? RosettaColors.online
: RosettaColors.Adaptive.textSecondary
)
.lineLimit(1)
if !subtitleText.isEmpty {
Text(subtitleText)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(
isTyping || (dialog?.isOnline == true)
? RosettaColors.online
: RosettaColors.Adaptive.textSecondary
)
.lineLimit(1)
}
}
.padding(.horizontal, 16)
.frame(height: 44)

View File

@@ -262,9 +262,7 @@ private extension ChatListView {
ToolbarItem(placement: .principal) {
HStack(spacing: 4) {
ToolbarStoriesAvatar()
Text("Chats")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(RosettaColors.Adaptive.text)
ToolbarTitleView()
}
}
@@ -302,9 +300,7 @@ private extension ChatListView {
ToolbarItem(placement: .principal) {
HStack(spacing: 4) {
ToolbarStoriesAvatar()
Text("Chats")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(RosettaColors.Adaptive.text)
ToolbarTitleView()
}
}
@@ -355,6 +351,30 @@ private struct ChatListToolbarBackgroundModifier: ViewModifier {
}
}
// MARK: - Toolbar Title (observation-isolated)
/// Reads `ProtocolManager.shared.connectionState` in its own observation scope.
/// Connection state changes during handshake (4+ rapid transitions) are absorbed here,
/// not cascaded to the parent ChatListView / NavigationStack.
private struct ToolbarTitleView: View {
var body: some View {
let state = ProtocolManager.shared.connectionState
let title: String = switch state {
case .disconnected: "Connecting..."
case .connecting: "Connecting..."
case .connected: "Connected"
case .handshaking: "Authenticating..."
case .deviceVerificationRequired: "Device Verification..."
case .authenticated: "Chats"
}
Text(title)
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(RosettaColors.Adaptive.text)
.contentTransition(.numericText())
.animation(.easeInOut(duration: 0.25), value: state)
}
}
// MARK: - Toolbar Stories Avatar (observation-isolated)
/// Reads `AccountManager` and `SessionManager` in its own observation scope.
@@ -470,23 +490,25 @@ private struct ChatListDialogContent: View {
.listRowSeparatorTint(RosettaColors.Adaptive.divider)
.alignmentGuide(.listRowSeparatorLeading) { _ in 82 }
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button {
viewModel.toggleMute(dialog)
Button(role: .destructive) {
withAnimation { viewModel.deleteDialog(dialog) }
} label: {
Label(
dialog.isMuted ? "Unmute" : "Mute",
systemImage: dialog.isMuted ? "bell" : "bell.slash"
)
Label("Delete", systemImage: "trash")
}
if !dialog.isSavedMessages {
Button {
viewModel.toggleMute(dialog)
} label: {
Label(
dialog.isMuted ? "Unmute" : "Mute",
systemImage: dialog.isMuted ? "bell" : "bell.slash"
)
}
.tint(dialog.isMuted ? .green : .indigo)
}
.tint(dialog.isMuted ? .green : .indigo)
}
.swipeActions(edge: .leading, allowsFullSwipe: true) {
Button {
viewModel.markAsRead(dialog)
} label: {
Label("Read", systemImage: "envelope.open")
}
.tint(RosettaColors.figmaBlue)
Button {
viewModel.togglePin(dialog)
} label: {