diff --git a/yobble/Views/Tab/ContactsTab.swift b/yobble/Views/Tab/ContactsTab.swift index 92a604c..a22a1c2 100644 --- a/yobble/Views/Tab/ContactsTab.swift +++ b/yobble/Views/Tab/ContactsTab.swift @@ -13,9 +13,13 @@ struct ContactsTab: View { @State private var creatingChatForContactId: UUID? @State private var pendingChatItem: PrivateChatListItem? @State private var isPendingChatActive = false + @State private var contactAvatars: [UUID: AvatarInfo] = [:] + @State private var avatarLoadedIds: Set = [] + @State private var avatarLoadingIds: Set = [] private let contactsService = ContactsService() private let chatService = ChatService() + private let profileService = ProfileService() private let pageSize = 25 private var currentUserId: String? { @@ -42,11 +46,17 @@ struct ContactsTab: View { Button { openChat(for: contact) } label: { - ContactRow(contact: contact, isLoading: creatingChatForContactId == contact.id) + ContactRow( + contact: contact, + avatarInfo: contactAvatars[contact.id], + currentUserId: currentUserId, + isLoading: creatingChatForContactId == contact.id + ) .contentShape(Rectangle()) } .buttonStyle(.plain) - .disabled(contact.isDeleted || creatingChatForContactId == contact.id) + .disabled(creatingChatForContactId == contact.id) +// .disabled(contact.isDeleted || creatingChatForContactId == contact.id) .contextMenu { Button { handleContactAction(.edit, for: contact) @@ -80,6 +90,7 @@ struct ContactsTab: View { } .listRowInsets(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12)) .onAppear { + loadAvatarIfNeeded(for: contact) if index >= contacts.count - 5 { Task { await loadContacts(reset: false) @@ -341,35 +352,56 @@ struct ContactsTab: View { return NSLocalizedString("Произошла неизвестная ошибка. Попробуйте позже.", comment: "Chat creation unknown error") } + + private func loadAvatarIfNeeded(for contact: Contact) { + guard !contact.isDeleted else { return } + let contactId = contact.id + if avatarLoadedIds.contains(contactId) || avatarLoadingIds.contains(contactId) { + return + } + + avatarLoadingIds.insert(contactId) + + Task { + do { + let profile = try await profileService.fetchProfile(userId: contactId) + await MainActor.run { + if let info = profile.avatars?.current { + contactAvatars[contactId] = info + } + avatarLoadedIds.insert(contactId) + avatarLoadingIds.remove(contactId) + } + } catch { + if AppConfig.DEBUG { + print("[ContactsTab] load avatar failed for \(contactId): \(error)") + } + await MainActor.run { + avatarLoadingIds.remove(contactId) + } + } + } + } } private struct ContactRow: View { let contact: Contact + let avatarInfo: AvatarInfo? + let currentUserId: String? let isLoading: Bool - init(contact: Contact, isLoading: Bool = false) { + private let avatarSize: CGFloat = 40 + + init(contact: Contact, avatarInfo: AvatarInfo? = nil, currentUserId: String? = nil, isLoading: Bool = false) { self.contact = contact + self.avatarInfo = avatarInfo + self.currentUserId = currentUserId self.isLoading = isLoading } var body: some View { - HStack(alignment: .top, spacing: 10) { - Circle() - .fill(contact.isDeleted ? Color(.systemGray5) : Color.accentColor.opacity(0.15)) - .frame(width: 40, height: 40) - .overlay( - Group { - if contact.isDeleted { - Image(systemName: "person.slash") - .font(.headline) - .foregroundColor(Color(.systemGray2)) - } else { - Text(contact.initials) - .font(.headline) - .foregroundColor(.accentColor) - } - } - ) + HStack(alignment: .top, spacing: 12) { + avatarView VStack(alignment: .leading, spacing: 3) { HStack(alignment: .firstTextBaseline) { @@ -411,6 +443,56 @@ private struct ContactRow: View { .padding(.vertical, 6) } + @ViewBuilder + private var avatarView: some View { + if let fileId = avatarInfo?.fileId, + let url = avatarURL(for: fileId), + let currentUserId { + CachedAvatarView(url: url, fileId: fileId, userId: currentUserId) { + placeholderAvatar + } + .aspectRatio(contentMode: .fill) + .frame(width: avatarSize, height: avatarSize) + .clipShape(Circle()) + } else { + placeholderAvatar + } + } + + private func avatarURL(for fileId: String) -> URL? { + let userId = contact.id.uuidString + let path = "\(AppConfig.API_SERVER)/v1/storage/download/avatar/\(userId)?file_id=\(fileId)" + return URL(string: path) + } + + private var placeholderAvatar: some View { + Circle() + .fill(avatarBackgroundColor) + .frame(width: avatarSize, height: avatarSize) + .overlay( + Group { + if contact.isDeleted { + Image(systemName: "person.slash") + .symbolRenderingMode(.hierarchical) + .font(.system(size: avatarSize * 0.45, weight: .semibold)) + .foregroundColor(avatarTextColor) + } else { + Text(contact.initials) + .font(.system(size: avatarSize * 0.5, weight: .semibold)) + .foregroundColor(avatarTextColor) + } + } + ) + } + + private var avatarBackgroundColor: Color { + contact.isDeleted ? Color(.systemGray5) : Color.accentColor.opacity(0.15) + } + + private var avatarTextColor: Color { + contact.isDeleted ? Color.accentColor : Color.accentColor + } + private var friendCodeBadge: some View { Text(NSLocalizedString("Код дружбы", comment: "Friend code badge")) .font(.caption2.weight(.medium))