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:
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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: "")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user