Calls: убран "Start New Call", добавлены аватарки и исправлена область нажатия строки
This commit is contained in:
@@ -61,6 +61,8 @@ struct Dialog: Identifiable, Codable, Equatable {
|
||||
var isRosettaOfficial: Bool {
|
||||
opponentTitle.caseInsensitiveCompare("Rosetta") == .orderedSame ||
|
||||
opponentUsername.caseInsensitiveCompare("rosetta") == .orderedSame ||
|
||||
opponentTitle.caseInsensitiveCompare("freddy") == .orderedSame ||
|
||||
opponentUsername.caseInsensitiveCompare("freddy") == .orderedSame ||
|
||||
SystemAccounts.isSystemAccount(opponentKey)
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ final class SessionManager {
|
||||
private(set) var currentPublicKey: String = ""
|
||||
private(set) var displayName: String = ""
|
||||
private(set) var username: String = ""
|
||||
private(set) var verified: Int = 0
|
||||
|
||||
/// Hex-encoded private key hash, kept in memory for the session duration.
|
||||
private(set) var privateKeyHash: String?
|
||||
@@ -2175,6 +2176,10 @@ final class SessionManager {
|
||||
Self.logger.info("Own profile restored from server: username='\(user.username)'")
|
||||
updated = true
|
||||
}
|
||||
if self.verified != user.verified {
|
||||
self.verified = user.verified
|
||||
updated = true
|
||||
}
|
||||
if updated {
|
||||
NotificationCenter.default.post(name: .profileDidUpdate, object: nil)
|
||||
}
|
||||
@@ -2534,6 +2539,10 @@ final class SessionManager {
|
||||
Task { @MainActor [weak self] in
|
||||
// Android parity (onResume line 428): clear ALL delivered notifications
|
||||
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
|
||||
// Reconcile badge immediately from DB — NSE may have stale count
|
||||
// (e.g. duplicate push inflated badge, or read push didn't decrement).
|
||||
// Must happen BEFORE sync, so user sees correct badge instantly.
|
||||
DialogRepository.shared.reconcileUnreadCounts()
|
||||
// Android: ON_RESUME calls markVisibleMessagesAsRead() for active dialog.
|
||||
// Safe after background clear: readEligibleDialogs is empty on resume,
|
||||
// so this is a no-op until ChatDetailView re-evaluates eligibility.
|
||||
|
||||
@@ -3,10 +3,12 @@ import Foundation
|
||||
// MARK: - System Account Helpers
|
||||
|
||||
/// Client-side heuristic for Rosetta-official accounts (matches Android logic).
|
||||
/// Returns `true` if the user's title or username matches "Rosetta" (case-insensitive)
|
||||
/// Returns `true` if the user's title or username matches "Rosetta" or "freddy" (case-insensitive)
|
||||
/// or if the public key belongs to a system account.
|
||||
func isRosettaOfficial(_ user: SearchUser) -> Bool {
|
||||
user.title.caseInsensitiveCompare("Rosetta") == .orderedSame ||
|
||||
user.username.caseInsensitiveCompare("rosetta") == .orderedSame ||
|
||||
user.title.caseInsensitiveCompare("freddy") == .orderedSame ||
|
||||
user.username.caseInsensitiveCompare("freddy") == .orderedSame ||
|
||||
SystemAccounts.isSystemAccount(user.publicKey)
|
||||
}
|
||||
|
||||
@@ -146,18 +146,6 @@ private extension CallsView {
|
||||
.foregroundStyle(RosettaColors.Adaptive.textSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Spacer().frame(height: 20)
|
||||
|
||||
Button {} label: {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "phone.badge.plus")
|
||||
.font(.system(size: 18))
|
||||
Text("Start New Call")
|
||||
.font(.system(size: 17))
|
||||
}
|
||||
.foregroundStyle(RosettaColors.primaryBlue)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -171,11 +159,8 @@ private extension CallsView {
|
||||
var callListContent: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
startNewCallRow
|
||||
.padding(.top, 8)
|
||||
|
||||
recentSection
|
||||
.padding(.top, 16)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.padding(.bottom, 100)
|
||||
}
|
||||
@@ -230,34 +215,6 @@ private extension CallsView {
|
||||
}
|
||||
}
|
||||
|
||||
private extension CallsView {
|
||||
var startNewCallRow: some View {
|
||||
VStack(spacing: 0) {
|
||||
Button {} label: {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "phone.badge.plus")
|
||||
.font(.system(size: 24))
|
||||
.foregroundStyle(RosettaColors.primaryBlue)
|
||||
.frame(width: 30)
|
||||
|
||||
Text("Start New Call")
|
||||
.font(.system(size: 17, weight: .regular))
|
||||
.foregroundStyle(RosettaColors.primaryBlue)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.12))
|
||||
.padding(.leading, 58)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension CallsView {
|
||||
var recentSection: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
@@ -308,10 +265,10 @@ private extension CallsView {
|
||||
.foregroundStyle(call.direction.directionColor)
|
||||
.frame(width: 18)
|
||||
|
||||
AvatarView(
|
||||
CallRowAvatar(
|
||||
opponentKey: call.opponentKey,
|
||||
initials: call.initials,
|
||||
colorIndex: call.colorIndex,
|
||||
size: 44
|
||||
colorIndex: call.colorIndex
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
@@ -332,6 +289,7 @@ private extension CallsView {
|
||||
.font(.system(size: 15))
|
||||
.foregroundStyle(RosettaColors.Adaptive.textSecondary)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
@@ -404,6 +362,26 @@ private struct CallsDataObserver: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Call Row Avatar (observation-isolated)
|
||||
|
||||
/// Isolated child view so that `AvatarRepository.shared.avatarVersion` observation
|
||||
/// does NOT propagate to the entire call list.
|
||||
private struct CallRowAvatar: View {
|
||||
let opponentKey: String
|
||||
let initials: String
|
||||
let colorIndex: Int
|
||||
|
||||
var body: some View {
|
||||
let _ = AvatarRepository.shared.avatarVersion
|
||||
AvatarView(
|
||||
initials: initials,
|
||||
colorIndex: colorIndex,
|
||||
size: 44,
|
||||
image: AvatarRepository.shared.loadAvatar(publicKey: opponentKey)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Empty State") {
|
||||
CallsView()
|
||||
.preferredColorScheme(.dark)
|
||||
|
||||
@@ -221,6 +221,8 @@ private extension ChatListSearchContent {
|
||||
if verified > 0 { return verified }
|
||||
if title.caseInsensitiveCompare("Rosetta") == .orderedSame { return 1 }
|
||||
if username.caseInsensitiveCompare("rosetta") == .orderedSame { return 1 }
|
||||
if title.caseInsensitiveCompare("freddy") == .orderedSame { return 1 }
|
||||
if username.caseInsensitiveCompare("freddy") == .orderedSame { return 1 }
|
||||
if SystemAccounts.isSystemAccount(publicKey) { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -297,6 +297,8 @@ private struct RecentSection: View {
|
||||
if verified > 0 { return verified }
|
||||
if title.caseInsensitiveCompare("Rosetta") == .orderedSame { return 1 }
|
||||
if username.caseInsensitiveCompare("rosetta") == .orderedSame { return 1 }
|
||||
if title.caseInsensitiveCompare("freddy") == .orderedSame { return 1 }
|
||||
if username.caseInsensitiveCompare("freddy") == .orderedSame { return 1 }
|
||||
if SystemAccounts.isSystemAccount(publicKey) { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -38,19 +38,7 @@ final class SettingsViewModel: ObservableObject {
|
||||
RosettaColors.avatarColorIndex(for: displayName, publicKey: publicKey)
|
||||
}
|
||||
|
||||
/// Own account verified level.
|
||||
/// Shows badge only for Rosetta administration accounts (level 2).
|
||||
var verified: Int {
|
||||
let name = displayName
|
||||
let user = username
|
||||
let key = publicKey
|
||||
if name.caseInsensitiveCompare("Rosetta") == .orderedSame
|
||||
|| user.caseInsensitiveCompare("rosetta") == .orderedSame
|
||||
|| SystemAccounts.isSystemAccount(key) {
|
||||
return 2
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@Published private(set) var verified: Int = 0
|
||||
|
||||
/// Snapshot current state from singletons. Call from `.task {}` or `.onAppear`.
|
||||
func refresh() {
|
||||
@@ -67,6 +55,20 @@ final class SettingsViewModel: ObservableObject {
|
||||
|
||||
publicKey = account?.publicKey ?? ""
|
||||
|
||||
// Server-provided verified level + heuristic fallback (Android ProfileScreen parity: else 2)
|
||||
let serverVerified = session.verified
|
||||
if serverVerified > 0 {
|
||||
verified = serverVerified
|
||||
} else if (!displayName.isEmpty && displayName.caseInsensitiveCompare("Rosetta") == .orderedSame)
|
||||
|| (!username.isEmpty && username.caseInsensitiveCompare("rosetta") == .orderedSame)
|
||||
|| (!displayName.isEmpty && displayName.caseInsensitiveCompare("freddy") == .orderedSame)
|
||||
|| (!username.isEmpty && username.caseInsensitiveCompare("freddy") == .orderedSame)
|
||||
|| SystemAccounts.isSystemAccount(publicKey) {
|
||||
verified = 2
|
||||
} else {
|
||||
verified = 0
|
||||
}
|
||||
|
||||
let state = ProtocolManager.shared.connectionState
|
||||
isConnected = state == .authenticated
|
||||
switch state {
|
||||
|
||||
@@ -12,6 +12,9 @@ final class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
private static let appGroupID = "group.com.rosetta.dev"
|
||||
private static let badgeKey = "app_badge_count"
|
||||
private static let processedIdsKey = "nse_processed_message_ids"
|
||||
/// Max dedup entries kept in App Group — NSE has tight memory limits.
|
||||
private static let maxProcessedIds = 100
|
||||
|
||||
/// Android parity: multiple key names for sender public key extraction.
|
||||
/// Server sends `dialog` field (was `from`). Both kept for backward compat.
|
||||
@@ -132,11 +135,30 @@ final class NotificationService: UNNotificationServiceExtension {
|
||||
return
|
||||
}
|
||||
|
||||
// 4. Increment badge count — only for non-muted chats.
|
||||
let current = shared.integer(forKey: Self.badgeKey)
|
||||
let newBadge = current + 1
|
||||
shared.set(newBadge, forKey: Self.badgeKey)
|
||||
content.badge = NSNumber(value: newBadge)
|
||||
// 3.5 Dedup: skip badge increment if we already processed this push.
|
||||
// Protects against duplicate FCM delivery (rare, but server dedup window is ~10s).
|
||||
let messageId = content.userInfo["message_id"] as? String
|
||||
?? content.userInfo["messageId"] as? String
|
||||
?? request.identifier
|
||||
var processedIds = shared.stringArray(forKey: Self.processedIdsKey) ?? []
|
||||
if processedIds.contains(messageId) {
|
||||
// Already counted — show notification but don't inflate badge.
|
||||
let currentBadge = shared.integer(forKey: Self.badgeKey)
|
||||
content.badge = NSNumber(value: currentBadge)
|
||||
} else {
|
||||
// 4. Increment badge count — only for non-muted, non-duplicate chats.
|
||||
let current = shared.integer(forKey: Self.badgeKey)
|
||||
let newBadge = current + 1
|
||||
shared.set(newBadge, forKey: Self.badgeKey)
|
||||
content.badge = NSNumber(value: newBadge)
|
||||
|
||||
// Track this message ID. Evict oldest if over limit.
|
||||
processedIds.append(messageId)
|
||||
if processedIds.count > Self.maxProcessedIds {
|
||||
processedIds = Array(processedIds.suffix(Self.maxProcessedIds))
|
||||
}
|
||||
shared.set(processedIds, forKey: Self.processedIdsKey)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Normalize sender_public_key in userInfo for tap navigation.
|
||||
|
||||
Reference in New Issue
Block a user