fix contact

This commit is contained in:
cheykrym 2025-12-03 23:10:36 +03:00
parent e44d56e71b
commit e9b43e76fa
2 changed files with 84 additions and 17 deletions

View File

@ -25,6 +25,11 @@ struct ContactPayload: Decodable {
let createdAt: Date let createdAt: Date
} }
struct ContactsListPayload: Decodable {
let items: [ContactPayload]
let hasMore: Bool
}
final class ContactsService { final class ContactsService {
private let client: NetworkClient private let client: NetworkClient
private let decoder: JSONDecoder private let decoder: JSONDecoder
@ -36,16 +41,20 @@ final class ContactsService {
self.decoder.dateDecodingStrategy = .custom(Self.decodeDate) self.decoder.dateDecodingStrategy = .custom(Self.decodeDate)
} }
func fetchContacts(completion: @escaping (Result<[ContactPayload], Error>) -> Void) { func fetchContacts(limit: Int, offset: Int, completion: @escaping (Result<ContactsListPayload, Error>) -> Void) {
client.request( client.request(
path: "/v1/user/contact/list", path: "/v1/user/contact/list",
method: .get, method: .get,
query: [
"limit": String(limit),
"offset": String(offset)
],
requiresAuth: true requiresAuth: true
) { [decoder] result in ) { [decoder] result in
switch result { switch result {
case .success(let response): case .success(let response):
do { do {
let apiResponse = try decoder.decode(APIResponse<[ContactPayload]>.self, from: response.data) let apiResponse = try decoder.decode(APIResponse<ContactsListPayload>.self, from: response.data)
guard apiResponse.status == "fine" else { guard apiResponse.status == "fine" else {
let message = apiResponse.detail ?? NSLocalizedString("Не удалось загрузить контакты.", comment: "Contacts service unexpected status") let message = apiResponse.detail ?? NSLocalizedString("Не удалось загрузить контакты.", comment: "Contacts service unexpected status")
completion(.failure(ContactsServiceError.unexpectedStatus(message))) completion(.failure(ContactsServiceError.unexpectedStatus(message)))
@ -71,9 +80,9 @@ final class ContactsService {
} }
} }
func fetchContacts() async throws -> [ContactPayload] { func fetchContacts(limit: Int, offset: Int) async throws -> ContactsListPayload {
try await withCheckedThrowingContinuation { continuation in try await withCheckedThrowingContinuation { continuation in
fetchContacts { result in fetchContacts(limit: limit, offset: offset) { result in
continuation.resume(with: result) continuation.resume(with: result)
} }
} }

View File

@ -5,9 +5,13 @@ struct ContactsTab: View {
@State private var contacts: [Contact] = [] @State private var contacts: [Contact] = []
@State private var isLoading = false @State private var isLoading = false
@State private var loadError: String? @State private var loadError: String?
@State private var pagingError: String?
@State private var activeAlert: ContactsAlert? @State private var activeAlert: ContactsAlert?
@State private var hasMore = true
@State private var offset = 0
private let contactsService = ContactsService() private let contactsService = ContactsService()
private let pageSize = 25
var body: some View { var body: some View {
List { List {
@ -20,7 +24,7 @@ struct ContactsTab: View {
} else if contacts.isEmpty { } else if contacts.isEmpty {
emptyState emptyState
} else { } else {
ForEach(contacts) { contact in ForEach(Array(contacts.enumerated()), id: \.element.id) { index, contact in
Button { Button {
showContactPlaceholder(for: contact) showContactPlaceholder(for: contact)
} label: { } label: {
@ -57,16 +61,29 @@ struct ContactsTab: View {
} }
} }
.listRowInsets(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12)) .listRowInsets(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12))
.onAppear {
if index >= contacts.count - 5 {
Task {
await loadContacts(reset: false)
}
}
}
}
if isLoading && !contacts.isEmpty {
loadingState
} else if let pagingError, !contacts.isEmpty {
pagingErrorState(pagingError)
} }
} }
} }
.background(Color(UIColor.systemBackground)) .background(Color(UIColor.systemBackground))
.listStyle(.plain) .listStyle(.plain)
.task { .task {
await loadContacts() await loadContacts(reset: false)
} }
.refreshable { .refreshable {
await loadContacts() await refreshContacts()
} }
.alert(item: $activeAlert) { alert in .alert(item: $activeAlert) { alert in
switch alert { switch alert {
@ -106,7 +123,25 @@ struct ContactsTab: View {
.font(.subheadline) .font(.subheadline)
.foregroundColor(.orange) .foregroundColor(.orange)
Spacer() Spacer()
Button(action: { Task { await loadContacts() } }) { Button(action: { Task { await refreshContacts() } }) {
Text(NSLocalizedString("Обновить", comment: "Contacts retry button"))
.font(.subheadline)
}
}
.padding(.vertical, 10)
.listRowInsets(EdgeInsets(top: 10, leading: 12, bottom: 10, trailing: 12))
.listRowSeparator(.hidden)
}
private func pagingErrorState(_ message: String) -> some View {
HStack(alignment: .center, spacing: 8) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
Text(message)
.font(.subheadline)
.foregroundColor(.orange)
Spacer()
Button(action: { Task { await loadContacts(reset: false) } }) {
Text(NSLocalizedString("Обновить", comment: "Contacts retry button")) Text(NSLocalizedString("Обновить", comment: "Contacts retry button"))
.font(.subheadline) .font(.subheadline)
} }
@ -136,20 +171,43 @@ struct ContactsTab: View {
} }
@MainActor @MainActor
private func loadContacts() async { private func refreshContacts() async {
if isLoading { hasMore = true
return offset = 0
} pagingError = nil
loadError = nil
contacts.removeAll()
await loadContacts(reset: true)
}
@MainActor
private func loadContacts(reset: Bool) async {
if isLoading { return }
if !reset && !hasMore { return }
isLoading = true isLoading = true
loadError = nil if offset == 0 {
loadError = nil
}
pagingError = nil
do { do {
let payloads = try await contactsService.fetchContacts() let payload = try await contactsService.fetchContacts(limit: pageSize, offset: offset)
contacts = payloads.map(Contact.init) let newContacts = payload.items.map(Contact.init)
if reset {
contacts = newContacts
} else {
contacts.append(contentsOf: newContacts)
}
offset += newContacts.count
hasMore = payload.hasMore
} catch { } catch {
loadError = error.localizedDescription let message = error.localizedDescription
// activeAlert = .error(message: error.localizedDescription) if contacts.isEmpty {
loadError = message
} else {
pagingError = message
}
if AppConfig.DEBUG { print("[ContactsTab] load contacts failed: \(error)") } if AppConfig.DEBUG { print("[ContactsTab] load contacts failed: \(error)") }
} }