- Added SystemAccounts enum to manage system account keys and titles. - Refactored Dialog model to replace isVerified with verified level. - Implemented effective verification logic for UI display in Dialog. - Updated DialogRepository to handle user verification levels. - Enhanced ProtocolManager and SessionManager to log user info with verification. - Modified AuthCoordinator to support back navigation to unlock screen. - Improved UnlockView and WelcomeView with new account creation flow. - Added VerifiedBadge component to visually represent account verification levels. - Updated ChatListView and SearchView to display verification badges for users. - Cleaned up debug print statements across various components.
247 lines
7.6 KiB
Swift
247 lines
7.6 KiB
Swift
import SwiftUI
|
|
|
|
// MARK: - ChatRowView
|
|
|
|
/// Chat row matching Figma "Row - Chats" component spec:
|
|
/// Row: height 78, paddingLeft 10, paddingRight 16, vertical center
|
|
/// Avatar: 62px circle, 10pt trailing padding
|
|
/// Title: SF Pro Medium 17pt, tracking -0.43, primary color
|
|
/// Message: SF Pro Regular 15pt, tracking -0.23, secondary color
|
|
/// Time: SF Pro Regular 14pt, tracking -0.23, secondary color
|
|
/// Badges gap: 6pt — verified 12px, muted 12px
|
|
/// Trailing: pt 8, pb 14 — readStatus + time (gap 2), pin/count at bottom
|
|
struct ChatRowView: View {
|
|
let dialog: Dialog
|
|
|
|
var body: some View {
|
|
HStack(spacing: 0) {
|
|
avatarSection
|
|
.padding(.trailing, 10)
|
|
|
|
contentSection
|
|
}
|
|
.padding(.leading, 10)
|
|
.padding(.trailing, 16)
|
|
.frame(height: 78)
|
|
.contentShape(Rectangle())
|
|
}
|
|
}
|
|
|
|
// MARK: - Avatar
|
|
|
|
private extension ChatRowView {
|
|
var avatarSection: some View {
|
|
AvatarView(
|
|
initials: dialog.initials,
|
|
colorIndex: dialog.avatarColorIndex,
|
|
size: 62,
|
|
isOnline: dialog.isOnline,
|
|
isSavedMessages: dialog.isSavedMessages
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Content Section (two-column: title+detail | trailing accessories)
|
|
|
|
private extension ChatRowView {
|
|
var contentSection: some View {
|
|
HStack(alignment: .center, spacing: 6) {
|
|
// Left column: title + message
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
titleRow
|
|
messageRow
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.clipped()
|
|
|
|
// Right column: time + pin/badge
|
|
trailingColumn
|
|
}
|
|
.frame(height: 63)
|
|
}
|
|
}
|
|
|
|
// MARK: - Title Row (name + badges)
|
|
|
|
private extension ChatRowView {
|
|
var titleRow: some View {
|
|
HStack(spacing: 4) {
|
|
Text(dialog.isSavedMessages ? "Saved Messages" : dialog.opponentTitle)
|
|
.font(.system(size: 17, weight: .medium))
|
|
.tracking(-0.43)
|
|
.foregroundStyle(RosettaColors.Adaptive.text)
|
|
.lineLimit(1)
|
|
|
|
if !dialog.isSavedMessages && dialog.effectiveVerified > 0 {
|
|
VerifiedBadge(
|
|
verified: dialog.effectiveVerified,
|
|
size: 12
|
|
)
|
|
}
|
|
|
|
if dialog.isMuted {
|
|
Image(systemName: "speaker.slash.fill")
|
|
.font(.system(size: 12))
|
|
.foregroundStyle(RosettaColors.Adaptive.textSecondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Message Row
|
|
|
|
private extension ChatRowView {
|
|
var messageRow: some View {
|
|
Text(messageText)
|
|
.font(.system(size: 15))
|
|
.tracking(-0.23)
|
|
.foregroundStyle(RosettaColors.Adaptive.textSecondary)
|
|
.lineLimit(1)
|
|
}
|
|
|
|
var messageText: String {
|
|
if dialog.lastMessage.isEmpty {
|
|
return "No messages yet"
|
|
}
|
|
return dialog.lastMessage
|
|
}
|
|
}
|
|
|
|
// MARK: - Trailing Column (time + delivery on top, pin/badge on bottom)
|
|
|
|
private extension ChatRowView {
|
|
var trailingColumn: some View {
|
|
VStack(alignment: .trailing, spacing: 0) {
|
|
// Top: read status + time
|
|
HStack(spacing: 2) {
|
|
if dialog.lastMessageFromMe && !dialog.isSavedMessages {
|
|
deliveryIcon
|
|
}
|
|
|
|
Text(formattedTime)
|
|
.font(.system(size: 14))
|
|
.tracking(-0.23)
|
|
.foregroundStyle(
|
|
dialog.unreadCount > 0 && !dialog.isMuted
|
|
? RosettaColors.figmaBlue
|
|
: RosettaColors.Adaptive.textSecondary
|
|
)
|
|
}
|
|
.padding(.top, 2)
|
|
|
|
Spacer(minLength: 0)
|
|
|
|
// Bottom: pin or unread badge
|
|
HStack(spacing: 8) {
|
|
if dialog.isPinned && dialog.unreadCount == 0 {
|
|
Image(systemName: "pin.fill")
|
|
.font(.system(size: 15))
|
|
.foregroundStyle(RosettaColors.Adaptive.textSecondary)
|
|
.rotationEffect(.degrees(45))
|
|
}
|
|
|
|
if dialog.unreadCount > 0 {
|
|
unreadBadge
|
|
}
|
|
}
|
|
.padding(.bottom, 2)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
var deliveryIcon: some View {
|
|
switch dialog.lastMessageDelivered {
|
|
case .waiting:
|
|
Image(systemName: "clock")
|
|
.font(.system(size: 13))
|
|
.foregroundStyle(RosettaColors.Adaptive.textSecondary)
|
|
case .delivered:
|
|
Image(systemName: "checkmark")
|
|
.font(.system(size: 12, weight: .bold))
|
|
.foregroundStyle(RosettaColors.Adaptive.textSecondary)
|
|
case .read:
|
|
Image(systemName: "checkmark")
|
|
.font(.system(size: 12, weight: .bold))
|
|
.foregroundStyle(RosettaColors.figmaBlue)
|
|
.overlay(alignment: .leading) {
|
|
Image(systemName: "checkmark")
|
|
.font(.system(size: 12, weight: .bold))
|
|
.foregroundStyle(RosettaColors.figmaBlue)
|
|
.offset(x: -4)
|
|
}
|
|
.padding(.trailing, 2)
|
|
case .error:
|
|
Image(systemName: "exclamationmark.circle.fill")
|
|
.font(.system(size: 14))
|
|
.foregroundStyle(RosettaColors.error)
|
|
}
|
|
}
|
|
|
|
var unreadBadge: some View {
|
|
let count = dialog.unreadCount
|
|
let text = count > 999 ? "\(count / 1000)K" : (count > 99 ? "99+" : "\(count)")
|
|
let isMuted = dialog.isMuted
|
|
|
|
return Text(text)
|
|
.font(.system(size: 15))
|
|
.tracking(-0.23)
|
|
.foregroundStyle(.white)
|
|
.padding(.horizontal, 4)
|
|
.frame(minWidth: 20, minHeight: 20)
|
|
.frame(maxWidth: 37)
|
|
.background {
|
|
Capsule()
|
|
.fill(isMuted ? Color(hex: 0x787880) : RosettaColors.figmaBlue)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Time Formatting
|
|
|
|
private extension ChatRowView {
|
|
var formattedTime: String {
|
|
guard dialog.lastMessageTimestamp > 0 else { return "" }
|
|
|
|
let date = Date(timeIntervalSince1970: Double(dialog.lastMessageTimestamp) / 1000)
|
|
let now = Date()
|
|
let calendar = Calendar.current
|
|
|
|
if calendar.isDateInToday(date) {
|
|
let f = DateFormatter()
|
|
f.dateFormat = "h:mm a"
|
|
return f.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)
|
|
} else {
|
|
let f = DateFormatter()
|
|
f.dateFormat = "dd.MM.yy"
|
|
return f.string(from: date)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview
|
|
|
|
#Preview {
|
|
let sampleDialog = Dialog(
|
|
id: "preview", account: "mykey", opponentKey: "abc001",
|
|
opponentTitle: "Alice Johnson",
|
|
opponentUsername: "alice",
|
|
lastMessage: "Hey, how are you?",
|
|
lastMessageTimestamp: Int64(Date().timeIntervalSince1970 * 1000),
|
|
unreadCount: 3, isOnline: true, lastSeen: 0,
|
|
verified: 1, iHaveSent: true,
|
|
isPinned: false, isMuted: false,
|
|
lastMessageFromMe: true, lastMessageDelivered: .read
|
|
)
|
|
|
|
VStack(spacing: 0) {
|
|
ChatRowView(dialog: sampleDialog)
|
|
}
|
|
.background(RosettaColors.Adaptive.background)
|
|
}
|