patch chats list
This commit is contained in:
		
							parent
							
								
									bc3ec97d67
								
							
						
					
					
						commit
						b8d22c814c
					
				@ -17,6 +17,10 @@ struct TopBarView: View {
 | 
			
		||||
        return title == "Home"
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    var isChatsTab: Bool {
 | 
			
		||||
        return title == "Chats"
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    var isProfileTab: Bool {
 | 
			
		||||
        return title == "Profile"
 | 
			
		||||
    }
 | 
			
		||||
@ -81,6 +85,15 @@ struct TopBarView: View {
 | 
			
		||||
                                .foregroundColor(.primary)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                } else if isChatsTab {
 | 
			
		||||
                    // Кнопка поиска
 | 
			
		||||
                    Button(action: {
 | 
			
		||||
                        // пока ничего не делаем
 | 
			
		||||
                    }) {
 | 
			
		||||
                        Image(systemName: "magnifyingglass")
 | 
			
		||||
                            .imageScale(.large)
 | 
			
		||||
                            .foregroundColor(.primary)
 | 
			
		||||
                    }
 | 
			
		||||
                } else if isProfileTab {
 | 
			
		||||
                    NavigationLink(destination: SettingsView(viewModel: viewModel)) {
 | 
			
		||||
                        Image(systemName: "wrench")
 | 
			
		||||
 | 
			
		||||
@ -98,6 +98,9 @@
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "Вы" : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "Вы предложили: %@" : {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
struct ChatsTab: View {
 | 
			
		||||
    var currentUserId: String? = nil
 | 
			
		||||
    @StateObject private var viewModel = PrivateChatsViewModel()
 | 
			
		||||
 | 
			
		||||
    var body: some View {
 | 
			
		||||
@ -53,7 +54,7 @@ struct ChatsTab: View {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            ForEach(viewModel.chats) { chat in
 | 
			
		||||
                ChatRowView(chat: chat)
 | 
			
		||||
                ChatRowView(chat: chat, currentUserId: currentUserId)
 | 
			
		||||
                    .contentShape(Rectangle())
 | 
			
		||||
                    .onAppear {
 | 
			
		||||
                        viewModel.loadMoreIfNeeded(currentItem: chat)
 | 
			
		||||
@ -126,6 +127,7 @@ struct ChatsTab: View {
 | 
			
		||||
 | 
			
		||||
private struct ChatRowView: View {
 | 
			
		||||
    let chat: PrivateChatListItem
 | 
			
		||||
    let currentUserId: String?
 | 
			
		||||
 | 
			
		||||
    private var title: String {
 | 
			
		||||
        switch chat.chatType {
 | 
			
		||||
@ -145,12 +147,47 @@ private struct ChatRowView: View {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var subtitle: String {
 | 
			
		||||
    private var officialFullName: String? {
 | 
			
		||||
        guard let name = chat.chatData?.fullName?.trimmingCharacters(in: .whitespacesAndNewlines), !name.isEmpty else {
 | 
			
		||||
            return nil
 | 
			
		||||
        }
 | 
			
		||||
        return name
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var loginDisplay: String? {
 | 
			
		||||
        guard let login = chat.chatData?.login?.trimmingCharacters(in: .whitespacesAndNewlines), !login.isEmpty else {
 | 
			
		||||
            return nil
 | 
			
		||||
        }
 | 
			
		||||
        return "@\(login)"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var isOfficial: Bool {
 | 
			
		||||
        officialFullName != nil
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var avatarBackgroundColor: Color {
 | 
			
		||||
        isOfficial ? Color.accentColor.opacity(0.85) : Color.accentColor.opacity(0.15)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var avatarTextColor: Color {
 | 
			
		||||
        isOfficial ? Color.white : Color.accentColor
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var messagePreview: String {
 | 
			
		||||
        guard let message = chat.lastMessage else {
 | 
			
		||||
            return NSLocalizedString("Нет сообщений", comment: "")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if let content = message.content, !content.isEmpty {
 | 
			
		||||
        let body = messageBody(for: message)
 | 
			
		||||
        guard let prefix = authorPrefix(for: message) else {
 | 
			
		||||
            return body
 | 
			
		||||
        }
 | 
			
		||||
        return body.isEmpty ? prefix : "\(body)"
 | 
			
		||||
//        return body.isEmpty ? prefix : "\(prefix): \(body)"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func messageBody(for message: MessageItem) -> String {
 | 
			
		||||
        if let content = trimmed(message.content), !content.isEmpty {
 | 
			
		||||
            return content
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -161,50 +198,158 @@ private struct ChatRowView: View {
 | 
			
		||||
        return NSLocalizedString("Сообщение", comment: "")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func authorPrefix(for message: MessageItem) -> String? {
 | 
			
		||||
        let isCurrentUser = currentUserId.map { $0 == message.senderId } ?? false
 | 
			
		||||
        if isCurrentUser {
 | 
			
		||||
            return NSLocalizedString("Вы", comment: "")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let profile = message.senderData ?? chat.chatData
 | 
			
		||||
        return displayName(for: profile)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func displayName(for profile: ChatProfile?) -> String? {
 | 
			
		||||
        guard let profile else { return nil }
 | 
			
		||||
 | 
			
		||||
        let login = trimmed(profile.login)
 | 
			
		||||
        let customName = trimmed(profile.customName)
 | 
			
		||||
        let fullName = trimmed(profile.fullName)
 | 
			
		||||
        let isOfficialProfile = fullName != nil
 | 
			
		||||
 | 
			
		||||
        if isOfficialProfile, let login {
 | 
			
		||||
            if let customName {
 | 
			
		||||
                return "@\(login) (\(customName))"
 | 
			
		||||
            }
 | 
			
		||||
            return "@\(login)"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if let customName {
 | 
			
		||||
            return customName
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if let login {
 | 
			
		||||
            return "@\(login)"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return fullName
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func trimmed(_ string: String?) -> String? {
 | 
			
		||||
        guard let string = string?.trimmingCharacters(in: .whitespacesAndNewlines), !string.isEmpty else {
 | 
			
		||||
            return nil
 | 
			
		||||
        }
 | 
			
		||||
        return string
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var timestamp: String? {
 | 
			
		||||
        let date = chat.lastMessage?.createdAt ?? chat.createdAt
 | 
			
		||||
        guard let date else { return nil }
 | 
			
		||||
        return ChatRowView.timeFormatter.string(from: date)
 | 
			
		||||
        return ChatRowView.formattedTimestamp(for: date)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var initial: String {
 | 
			
		||||
        return String(title.prefix(1)).uppercased()
 | 
			
		||||
        let sourceName: String
 | 
			
		||||
 | 
			
		||||
        if let full = officialFullName {
 | 
			
		||||
            sourceName = full
 | 
			
		||||
        } else if let custom = chat.chatData?.customName, !custom.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
 | 
			
		||||
            sourceName = custom
 | 
			
		||||
        } else if let login = chat.chatData?.login?.trimmingCharacters(in: .whitespacesAndNewlines), !login.isEmpty {
 | 
			
		||||
            sourceName = login
 | 
			
		||||
        } else {
 | 
			
		||||
            sourceName = NSLocalizedString("Неизвестный пользователь", comment: "")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if let character = sourceName.first(where: { !$0.isWhitespace && $0 != "@" }) {
 | 
			
		||||
            return String(character).uppercased()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return "?"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var subtitleColor: Color {
 | 
			
		||||
        chat.unreadCount > 0 ? .primary : .secondary
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var shouldShowReadStatus: Bool {
 | 
			
		||||
        guard let message = chat.lastMessage else { return false }
 | 
			
		||||
        let isCurrentUser = currentUserId.map { $0 == message.senderId } ?? false
 | 
			
		||||
        return isCurrentUser
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var readStatusIconName: String {
 | 
			
		||||
        guard let message = chat.lastMessage else { return "" }
 | 
			
		||||
        return message.isViewed == true ? "checkmark.circle.fill" : "checkmark.circle"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var readStatusColor: Color {
 | 
			
		||||
        guard let message = chat.lastMessage else { return .secondary }
 | 
			
		||||
        return message.isViewed == true ? Color.accentColor : Color.secondary
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        HStack(spacing: 12) {
 | 
			
		||||
            Circle()
 | 
			
		||||
                .fill(Color.accentColor.opacity(0.15))
 | 
			
		||||
                .fill(avatarBackgroundColor)
 | 
			
		||||
                .frame(width: 44, height: 44)
 | 
			
		||||
                .overlay(
 | 
			
		||||
                    Text(initial)
 | 
			
		||||
                        .font(.headline)
 | 
			
		||||
                        .foregroundColor(Color.accentColor)
 | 
			
		||||
                        .foregroundColor(avatarTextColor)
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            VStack(alignment: .leading, spacing: 4) {
 | 
			
		||||
                Text(title)
 | 
			
		||||
                    .fontWeight(chat.unreadCount > 0 ? .semibold : .regular)
 | 
			
		||||
                    .foregroundColor(.primary)
 | 
			
		||||
                    .lineLimit(1)
 | 
			
		||||
                if let officialName = officialFullName {
 | 
			
		||||
                    HStack(spacing: 6) {
 | 
			
		||||
                        Text(officialName)
 | 
			
		||||
                            .fontWeight(.semibold)
 | 
			
		||||
                            .foregroundColor(.primary)
 | 
			
		||||
                            .lineLimit(1)
 | 
			
		||||
                            .truncationMode(.tail)
 | 
			
		||||
 | 
			
		||||
                Text(subtitle)
 | 
			
		||||
                        Image(systemName: "checkmark.seal.fill")
 | 
			
		||||
                            .foregroundColor(Color.accentColor)
 | 
			
		||||
                            .font(.caption)
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
//                    if let login = loginDisplay {
 | 
			
		||||
//                        Text(login)
 | 
			
		||||
//                            .font(.footnote)
 | 
			
		||||
//                            .foregroundColor(.secondary)
 | 
			
		||||
//                            .lineLimit(1)
 | 
			
		||||
//                            .truncationMode(.tail)
 | 
			
		||||
//                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    Text(title)
 | 
			
		||||
                        .fontWeight(chat.unreadCount > 0 ? .semibold : .regular)
 | 
			
		||||
                        .foregroundColor(.primary)
 | 
			
		||||
                        .lineLimit(1)
 | 
			
		||||
                        .truncationMode(.tail)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Text(messagePreview)
 | 
			
		||||
                    .font(.subheadline)
 | 
			
		||||
                    .foregroundColor(subtitleColor)
 | 
			
		||||
                    .lineLimit(2)
 | 
			
		||||
                    .truncationMode(.tail)
 | 
			
		||||
            }
 | 
			
		||||
            .frame(maxWidth: .infinity, alignment: .leading)
 | 
			
		||||
 | 
			
		||||
            Spacer()
 | 
			
		||||
 | 
			
		||||
            VStack(alignment: .trailing, spacing: 6) {
 | 
			
		||||
                if let timestamp {
 | 
			
		||||
                    Text(timestamp)
 | 
			
		||||
                        .font(.caption)
 | 
			
		||||
                        .foregroundColor(.secondary)
 | 
			
		||||
                    HStack(spacing: 4) {
 | 
			
		||||
                        if shouldShowReadStatus {
 | 
			
		||||
                            Image(systemName: readStatusIconName)
 | 
			
		||||
                                .foregroundColor(readStatusColor)
 | 
			
		||||
                                .font(.caption2)
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        Text(timestamp)
 | 
			
		||||
                            .font(.caption)
 | 
			
		||||
                            .foregroundColor(.secondary)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if chat.unreadCount > 0 {
 | 
			
		||||
@ -222,12 +367,68 @@ private struct ChatRowView: View {
 | 
			
		||||
        .padding(.vertical, 8)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static let timeFormatter: DateFormatter = {
 | 
			
		||||
    private static func formattedTimestamp(for date: Date) -> String {
 | 
			
		||||
        let calendar = Calendar.current
 | 
			
		||||
        let locale = Locale.current
 | 
			
		||||
 | 
			
		||||
        let now = Date()
 | 
			
		||||
        let startOfNow = calendar.startOfDay(for: now)
 | 
			
		||||
        let startOfDate = calendar.startOfDay(for: date)
 | 
			
		||||
        let diff = calendar.dateComponents([.day], from: startOfDate, to: startOfNow).day ?? 0
 | 
			
		||||
 | 
			
		||||
        if diff <= 0 {
 | 
			
		||||
            return timeString(for: date, locale: locale)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if diff == 1 {
 | 
			
		||||
            return locale.identifier.lowercased().hasPrefix("ru") ? "Вчера" : "Yesterday"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if diff < 7 {
 | 
			
		||||
            return weekdayFormatter(for: locale).string(from: date)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if diff < 365 {
 | 
			
		||||
            return monthDayFormatter(for: locale).string(from: date)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return fullDateFormatter(for: locale).string(from: date)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static func timeString(for date: Date, locale: Locale) -> String {
 | 
			
		||||
        let timeFormat = DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: locale) ?? ""
 | 
			
		||||
        let is12Hour = timeFormat.contains("a") || timeFormat.contains("h") || timeFormat.contains("K")
 | 
			
		||||
 | 
			
		||||
        let formatter = DateFormatter()
 | 
			
		||||
        formatter.dateStyle = .short
 | 
			
		||||
        formatter.timeStyle = .short
 | 
			
		||||
        formatter.locale = locale
 | 
			
		||||
        if is12Hour {
 | 
			
		||||
            formatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "h:mm a", options: 0, locale: locale) ?? "h:mm a"
 | 
			
		||||
        } else {
 | 
			
		||||
            formatter.dateFormat = "HH:mm"
 | 
			
		||||
        }
 | 
			
		||||
        return formatter.string(from: date)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static func weekdayFormatter(for locale: Locale) -> DateFormatter {
 | 
			
		||||
        let formatter = DateFormatter()
 | 
			
		||||
        formatter.locale = locale
 | 
			
		||||
        formatter.setLocalizedDateFormatFromTemplate("EEE")
 | 
			
		||||
        return formatter
 | 
			
		||||
    }()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static func monthDayFormatter(for locale: Locale) -> DateFormatter {
 | 
			
		||||
        let formatter = DateFormatter()
 | 
			
		||||
        formatter.locale = locale
 | 
			
		||||
        formatter.setLocalizedDateFormatFromTemplate("MMM d")
 | 
			
		||||
        return formatter
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static func fullDateFormatter(for locale: Locale) -> DateFormatter {
 | 
			
		||||
        let formatter = DateFormatter()
 | 
			
		||||
        formatter.locale = locale
 | 
			
		||||
        formatter.dateFormat = "dd.MM.yy"
 | 
			
		||||
        return formatter
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct ChatsTab_Previews: PreviewProvider {
 | 
			
		||||
 | 
			
		||||
@ -48,7 +48,7 @@ struct MainView: View {
 | 
			
		||||
                        FeedbackTab()
 | 
			
		||||
                            .opacity(selectedTab == 1 ? 1 : 0)
 | 
			
		||||
                        
 | 
			
		||||
                        ChatsTab()
 | 
			
		||||
                        ChatsTab(currentUserId: viewModel.userId.isEmpty ? nil : viewModel.userId)
 | 
			
		||||
                            .opacity(selectedTab == 2 ? 1 : 0)
 | 
			
		||||
                        
 | 
			
		||||
                        ProfileTab()
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user