encryptWithPassword возвращён к SHA256+rawDeflate (iOS-only данные)

Добавлен encryptWithPasswordDesktopCompat (SHA1+zlibDeflate) для кросс-платформенных данных (aesChachaKey, аватар)
3 вызова в SessionManager переведены на desktop-compatible путь
Добавлен Notification.Name.profileDidUpdate для мгновенного обновления имени в Settings
Удалены debug-логи из CryptoManager и SessionManager
This commit is contained in:
2026-03-15 03:50:56 +05:00
parent acc3fb8e2f
commit dd4642f251
48 changed files with 3865 additions and 517 deletions

View File

@@ -182,6 +182,10 @@ private extension ChatListSearchContent {
name: user.title, publicKey: user.publicKey
)
let colorIdx = RosettaColors.avatarColorIndex(for: user.title, publicKey: user.publicKey)
let effectiveVerified = Self.effectiveVerifiedLevel(
verified: user.verified, title: user.title,
username: user.username, publicKey: user.publicKey
)
return Button {
onOpenDialog(ChatRoute(recent: user))
@@ -193,16 +197,28 @@ private extension ChatListSearchContent {
)
VStack(alignment: .leading, spacing: 1) {
Text(isSelf ? "Saved Messages" : (
user.title.isEmpty
? String(user.publicKey.prefix(16)) + "..."
: user.title
))
.font(.system(size: 17, weight: .medium))
.foregroundStyle(RosettaColors.Adaptive.text)
.lineLimit(1)
if !user.lastSeenText.isEmpty {
Text(user.lastSeenText)
HStack(spacing: 4) {
Text(isSelf ? "Saved Messages" : (
user.title.isEmpty
? String(user.publicKey.prefix(16)) + "..."
: user.title
))
.font(.system(size: 17, weight: .medium))
.foregroundStyle(RosettaColors.Adaptive.text)
.lineLimit(1)
if !isSelf && effectiveVerified > 0 {
VerifiedBadge(
verified: effectiveVerified,
size: 14
)
}
}
// Desktop parity: search subtitle shows @username, not online/offline.
if !isSelf {
Text(user.username.isEmpty
? "@\(String(user.publicKey.prefix(10)))..."
: "@\(user.username)"
)
.font(.system(size: 13))
.foregroundStyle(RosettaColors.Adaptive.textSecondary)
.lineLimit(1)
@@ -216,6 +232,17 @@ private extension ChatListSearchContent {
}
.buttonStyle(.plain)
}
/// Desktop parity: compute effective verified level server value + client heuristic.
private static func effectiveVerifiedLevel(
verified: Int, title: String, username: String, publicKey: String
) -> Int {
if verified > 0 { return verified }
if title.caseInsensitiveCompare("Rosetta") == .orderedSame { return 1 }
if username.caseInsensitiveCompare("rosetta") == .orderedSame { return 1 }
if SystemAccounts.isSystemAccount(publicKey) { return 1 }
return 0
}
}
// MARK: - Server User Row

View File

@@ -355,21 +355,58 @@ 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,
/// Reads `ProtocolManager.shared.connectionState` and `SessionManager.shared.syncBatchInProgress`
/// in its own observation scope. State changes 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 .authenticated: "Chats"
default: "Connecting..."
let isSyncing = SessionManager.shared.syncBatchInProgress
if state == .authenticated && isSyncing {
UpdatingDotsView()
} else {
let title: String = switch state {
case .authenticated: "Chats"
default: "Connecting..."
}
Text(title)
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(RosettaColors.Adaptive.text)
.contentTransition(.numericText())
.animation(.easeInOut(duration: 0.25), value: state)
}
}
}
/// Desktop parity: "Updating..." with bouncing dots animation during sync.
private struct UpdatingDotsView: View {
@State private var activeDot = 0
private let dotCount = 3
private let timer = Timer.publish(every: 0.4, on: .main, in: .common).autoconnect()
var body: some View {
HStack(spacing: 1) {
Text("Updating")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(RosettaColors.Adaptive.text)
HStack(spacing: 2) {
ForEach(0..<dotCount, id: \.self) { index in
Text(".")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(RosettaColors.Adaptive.text)
.offset(y: activeDot == index ? -3 : 0)
.animation(
.easeInOut(duration: 0.3),
value: activeDot
)
}
}
}
.onReceive(timer) { _ in
activeDot = (activeDot + 1) % dotCount
}
Text(title)
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(RosettaColors.Adaptive.text)
.contentTransition(.numericText())
.animation(.easeInOut(duration: 0.25), value: state)
}
}
@@ -385,7 +422,34 @@ private struct ToolbarStoriesAvatar: View {
name: SessionManager.shared.displayName, publicKey: pk
)
let colorIdx = RosettaColors.avatarColorIndex(for: SessionManager.shared.displayName, publicKey: pk)
ZStack { AvatarView(initials: initials, colorIndex: colorIdx, size: 28) }
// Reading avatarVersion triggers observation re-renders when any avatar is saved/removed.
let _ = AvatarRepository.shared.avatarVersion
let avatar = AvatarRepository.shared.loadAvatar(publicKey: pk)
ZStack { AvatarView(initials: initials, colorIndex: colorIdx, size: 28, image: avatar) }
}
}
// MARK: - Sync-Aware Empty State (observation-isolated)
/// Shows "Syncing..." indicator when sync is in progress, otherwise shows empty state.
/// Reads `SessionManager.syncBatchInProgress` in its own observation scope.
private struct SyncAwareEmptyState: View {
var body: some View {
let isSyncing = SessionManager.shared.syncBatchInProgress
if isSyncing {
VStack(spacing: 16) {
Spacer().frame(height: 120)
ProgressView()
.tint(.white)
Text("Syncing conversations…")
.font(.system(size: 15))
.foregroundStyle(Color.white.opacity(0.5))
Spacer()
}
.frame(maxWidth: .infinity)
} else {
ChatEmptyStateView(searchText: "")
}
}
}
@@ -427,7 +491,7 @@ private struct ChatListDialogContent: View {
var body: some View {
let hasPinned = !viewModel.pinnedDialogs.isEmpty
if viewModel.filteredDialogs.isEmpty && !viewModel.isLoading {
ChatEmptyStateView(searchText: "")
SyncAwareEmptyState()
.onChange(of: hasPinned) { _, val in onPinnedStateChange(val) }
.onAppear { onPinnedStateChange(hasPinned) }
.onReceive(MessageRepository.shared.$typingDialogs) { typingDialogs = $0 }

View File

@@ -61,7 +61,8 @@ private extension ChatRowView {
colorIndex: dialog.avatarColorIndex,
size: 62,
isOnline: dialog.isOnline,
isSavedMessages: dialog.isSavedMessages
isSavedMessages: dialog.isSavedMessages,
image: AvatarRepository.shared.loadAvatar(publicKey: dialog.opponentKey)
)
}
}
@@ -144,7 +145,8 @@ private extension ChatRowView {
if dialog.lastMessage.isEmpty {
return "No messages yet"
}
return dialog.lastMessage
// Strip inline markdown markers for clean chat list preview
return dialog.lastMessage.replacingOccurrences(of: "**", with: "")
}
}