add contact add

This commit is contained in:
cheykrym 2025-12-11 04:29:55 +03:00
parent c2177278e2
commit a1446ec8bf
5 changed files with 184 additions and 13 deletions

View File

@ -331,6 +331,20 @@ struct RelationshipStatus: Decodable {
}
}
extension RelationshipStatus {
init(
isTargetInContactsOfCurrentUser: Bool,
isCurrentUserInContactsOfTarget: Bool,
isTargetUserBlockedByCurrentUser: Bool,
isCurrentUserInBlacklistOfTarget: Bool
) {
self.isTargetInContactsOfCurrentUser = isTargetInContactsOfCurrentUser
self.isCurrentUserInContactsOfTarget = isCurrentUserInContactsOfTarget
self.isTargetUserBlockedByCurrentUser = isTargetUserBlockedByCurrentUser
self.isCurrentUserInBlacklistOfTarget = isCurrentUserInBlacklistOfTarget
}
}
enum JSONValue: Decodable {
case string(String)
case int(Int)

View File

@ -3,6 +3,7 @@ import Foundation
enum ContactsServiceError: LocalizedError {
case unexpectedStatus(String)
case decoding(debugDescription: String)
case encoding(String)
var errorDescription: String? {
switch self {
@ -12,6 +13,8 @@ enum ContactsServiceError: LocalizedError {
return AppConfig.DEBUG
? debugDescription
: NSLocalizedString("Не удалось загрузить контакты.", comment: "Contacts service decoding error")
case .encoding(let message):
return message
}
}
}
@ -30,15 +33,26 @@ struct ContactsListPayload: Decodable {
let hasMore: Bool
}
private struct ContactCreateRequestPayload: Encodable {
let userId: UUID?
let login: String?
let friendCode: String?
let customName: String?
}
final class ContactsService {
private let client: NetworkClient
private let decoder: JSONDecoder
private let encoder: JSONEncoder
init(client: NetworkClient = .shared) {
self.client = client
self.decoder = JSONDecoder()
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
self.decoder.dateDecodingStrategy = .custom(Self.decodeDate)
self.encoder = JSONEncoder()
self.encoder.keyEncodingStrategy = .convertToSnakeCase
}
func fetchContacts(limit: Int, offset: Int, completion: @escaping (Result<ContactsListPayload, Error>) -> Void) {
@ -88,6 +102,63 @@ final class ContactsService {
}
}
func addContact(userId: UUID, customName: String?, completion: @escaping (Result<ContactPayload, Error>) -> Void) {
let request = ContactCreateRequestPayload(
userId: userId,
login: nil,
friendCode: nil,
customName: customName
)
guard let body = try? encoder.encode(request) else {
let message = NSLocalizedString("Не удалось подготовить данные запроса.", comment: "Contacts service encoding error")
completion(.failure(ContactsServiceError.encoding(message)))
return
}
client.request(
path: "/v1/user/contact/add",
method: .post,
body: body,
requiresAuth: true
) { [decoder] result in
switch result {
case .success(let response):
do {
let apiResponse = try decoder.decode(APIResponse<ContactPayload>.self, from: response.data)
guard apiResponse.status == "fine" else {
let message = apiResponse.detail ?? NSLocalizedString("Не удалось добавить контакт.", comment: "Contacts service add unexpected status")
completion(.failure(ContactsServiceError.unexpectedStatus(message)))
return
}
completion(.success(apiResponse.data))
} catch {
let debugMessage = Self.describeDecodingError(error: error, data: response.data)
if AppConfig.DEBUG {
print("[ContactsService] decode contact add failed: \(debugMessage)")
}
completion(.failure(ContactsServiceError.decoding(debugDescription: debugMessage)))
}
case .failure(let error):
if case let NetworkError.server(_, data) = error,
let data,
let message = Self.errorMessage(from: data) {
completion(.failure(ContactsServiceError.unexpectedStatus(message)))
return
}
completion(.failure(error))
}
}
}
func addContact(userId: UUID, customName: String?) async throws -> ContactPayload {
try await withCheckedThrowingContinuation { continuation in
addContact(userId: userId, customName: customName) { result in
continuation.resume(with: result)
}
}
}
private static func decodeDate(from decoder: Decoder) throws -> Date {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)

View File

@ -581,9 +581,6 @@
"Добавить контакт" : {
"comment" : "Message profile add contact alert title"
},
"Добавление контакта появится позже." : {
"comment" : "Contact add placeholder message"
},
"Добавление новых блокировок появится позже." : {
"comment" : "Add blocked user placeholder message"
},
@ -856,6 +853,9 @@
},
"Имя в чате" : {
},
"Имя не может быть пустым." : {
"comment" : "Contact add empty name error"
},
"Имя, логин и статус — как в профиле Telegram." : {
"comment" : "Message profile about description"
@ -1337,6 +1337,9 @@
"Не удалось выполнить поиск." : {
"comment" : "Search error fallback\nSearch service decoding error"
},
"Не удалось добавить контакт." : {
"comment" : "Contacts service add unexpected status"
},
"Не удалось заблокировать пользователя." : {
"comment" : "Blocked users create unexpected status"
},
@ -1441,6 +1444,9 @@
"Не удалось определить пользователя для блокировки." : {
"comment" : "Message profile missing user id error"
},
"Не удалось определить пользователя для добавления." : {
"comment" : "Contact add invalid user id error"
},
"Не удалось открыть чат" : {
"comment" : "Chat creation error title"
},
@ -1454,7 +1460,7 @@
},
"Не удалось подготовить данные запроса." : {
"comment" : "Blocked users create encoding error\nBlocked users delete encoding error\nProfile update encoding error"
"comment" : "Blocked users create encoding error\nBlocked users delete encoding error\nContacts service encoding error\nProfile update encoding error"
},
"Не удалось подготовить изображение для загрузки." : {
"comment" : "Avatar encoding error"

View File

@ -289,7 +289,9 @@ struct MessageProfileView: View {
rowDivider
if let profile = currentChatProfile {
NavigationLink {
ContactAddView(contact: ContactEditInfo(profile: profile))
ContactAddView(contact: ContactEditInfo(profile: profile)) { payload in
handleContactAdded(payload)
}
} label: {
filledActionLabel(
title: NSLocalizedString("Добавить в контакты", comment: "Message profile add to contacts title"),
@ -563,6 +565,37 @@ struct MessageProfileView: View {
)
}
private func handleContactAdded(_ payload: ContactPayload) {
guard let profile = currentChatProfile else { return }
let existingRelationship = profile.relationship
let updatedRelationship = RelationshipStatus(
isTargetInContactsOfCurrentUser: true,
isCurrentUserInContactsOfTarget: existingRelationship?.isCurrentUserInContactsOfTarget ?? false,
isTargetUserBlockedByCurrentUser: existingRelationship?.isTargetUserBlockedByCurrentUser ?? false,
isCurrentUserInBlacklistOfTarget: existingRelationship?.isCurrentUserInBlacklistOfTarget ?? false
)
let updatedProfile = ChatProfile(
userId: profile.userId,
login: profile.login,
fullName: profile.fullName,
customName: payload.customName ?? profile.customName,
bio: profile.bio,
lastSeen: profile.lastSeen,
createdAt: profile.createdAt,
avatars: profile.avatars,
stories: profile.stories,
permissions: profile.permissions,
profilePermissions: profile.profilePermissions,
relationship: updatedRelationship,
rating: profile.rating,
isOfficial: profile.isOfficial
)
chatProfile = updatedProfile
}
private func handleEditContactTap() {
showPlaceholderAction(
title: NSLocalizedString("Ошибка", comment: "Common error title"),

View File

@ -2,12 +2,18 @@ import SwiftUI
struct ContactAddView: View {
let contact: ContactEditInfo
let onContactAdded: ((ContactPayload) -> Void)?
@Environment(\.dismiss) private var dismiss
private let contactsService = ContactsService()
@State private var displayName: String
@State private var activeAlert: ContactAddAlert?
@State private var isSaving = false
init(contact: ContactEditInfo) {
init(contact: ContactEditInfo, onContactAdded: ((ContactPayload) -> Void)? = nil) {
self.contact = contact
self.onContactAdded = onContactAdded
let initialName = contact.preferredName
_displayName = State(initialValue: initialName)
}
@ -18,18 +24,23 @@ struct ContactAddView: View {
Section(header: Text(NSLocalizedString("Публичная информация", comment: "Contact add public info section title"))) {
TextField(NSLocalizedString("Отображаемое имя", comment: "Display name field placeholder"), text: $displayName)
.disabled(isSaving)
}
}
.navigationTitle(NSLocalizedString("Новый контакт", comment: "Contact add title"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
if isSaving {
ProgressView()
} else {
Button(NSLocalizedString("Сохранить", comment: "Contact add save button")) {
handleSaveTap()
}
.disabled(!hasChanges)
}
}
}
.alert(item: $activeAlert) { item in
Alert(
title: Text(item.title),
@ -116,10 +127,46 @@ struct ContactAddView: View {
}
private func handleSaveTap() {
guard !isSaving else { return }
guard let userId = UUID(uuidString: contact.userId) else {
activeAlert = ContactAddAlert(
title: NSLocalizedString("Скоро", comment: "Common soon title"),
message: NSLocalizedString("Добавление контакта появится позже.", comment: "Contact add placeholder message")
title: NSLocalizedString("Ошибка", comment: "Common error title"),
message: NSLocalizedString("Не удалось определить пользователя для добавления.", comment: "Contact add invalid user id error")
)
return
}
let trimmedName = displayName.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
guard !trimmedName.isEmpty else {
activeAlert = ContactAddAlert(
title: NSLocalizedString("Ошибка", comment: "Common error title"),
message: NSLocalizedString("Имя не может быть пустым.", comment: "Contact add empty name error")
)
return
}
isSaving = true
let customName = trimmedName
Task {
do {
let payload = try await contactsService.addContact(userId: userId, customName: customName)
await MainActor.run {
isSaving = false
onContactAdded?(payload)
dismiss()
}
} catch {
await MainActor.run {
isSaving = false
activeAlert = ContactAddAlert(
title: NSLocalizedString("Ошибка", comment: "Common error title"),
message: error.localizedDescription
)
}
}
}
}
}