add contact add
This commit is contained in:
parent
c2177278e2
commit
a1446ec8bf
@ -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 {
|
enum JSONValue: Decodable {
|
||||||
case string(String)
|
case string(String)
|
||||||
case int(Int)
|
case int(Int)
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import Foundation
|
|||||||
enum ContactsServiceError: LocalizedError {
|
enum ContactsServiceError: LocalizedError {
|
||||||
case unexpectedStatus(String)
|
case unexpectedStatus(String)
|
||||||
case decoding(debugDescription: String)
|
case decoding(debugDescription: String)
|
||||||
|
case encoding(String)
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
@ -12,6 +13,8 @@ enum ContactsServiceError: LocalizedError {
|
|||||||
return AppConfig.DEBUG
|
return AppConfig.DEBUG
|
||||||
? debugDescription
|
? debugDescription
|
||||||
: NSLocalizedString("Не удалось загрузить контакты.", comment: "Contacts service decoding error")
|
: NSLocalizedString("Не удалось загрузить контакты.", comment: "Contacts service decoding error")
|
||||||
|
case .encoding(let message):
|
||||||
|
return message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -30,15 +33,26 @@ struct ContactsListPayload: Decodable {
|
|||||||
let hasMore: Bool
|
let hasMore: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct ContactCreateRequestPayload: Encodable {
|
||||||
|
let userId: UUID?
|
||||||
|
let login: String?
|
||||||
|
let friendCode: String?
|
||||||
|
let customName: String?
|
||||||
|
}
|
||||||
|
|
||||||
final class ContactsService {
|
final class ContactsService {
|
||||||
private let client: NetworkClient
|
private let client: NetworkClient
|
||||||
private let decoder: JSONDecoder
|
private let decoder: JSONDecoder
|
||||||
|
private let encoder: JSONEncoder
|
||||||
|
|
||||||
init(client: NetworkClient = .shared) {
|
init(client: NetworkClient = .shared) {
|
||||||
self.client = client
|
self.client = client
|
||||||
self.decoder = JSONDecoder()
|
self.decoder = JSONDecoder()
|
||||||
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
|
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
self.decoder.dateDecodingStrategy = .custom(Self.decodeDate)
|
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) {
|
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 {
|
private static func decodeDate(from decoder: Decoder) throws -> Date {
|
||||||
let container = try decoder.singleValueContainer()
|
let container = try decoder.singleValueContainer()
|
||||||
let string = try container.decode(String.self)
|
let string = try container.decode(String.self)
|
||||||
|
|||||||
@ -581,9 +581,6 @@
|
|||||||
"Добавить контакт" : {
|
"Добавить контакт" : {
|
||||||
"comment" : "Message profile add contact alert title"
|
"comment" : "Message profile add contact alert title"
|
||||||
},
|
},
|
||||||
"Добавление контакта появится позже." : {
|
|
||||||
"comment" : "Contact add placeholder message"
|
|
||||||
},
|
|
||||||
"Добавление новых блокировок появится позже." : {
|
"Добавление новых блокировок появится позже." : {
|
||||||
"comment" : "Add blocked user placeholder message"
|
"comment" : "Add blocked user placeholder message"
|
||||||
},
|
},
|
||||||
@ -856,6 +853,9 @@
|
|||||||
},
|
},
|
||||||
"Имя в чате" : {
|
"Имя в чате" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Имя не может быть пустым." : {
|
||||||
|
"comment" : "Contact add empty name error"
|
||||||
},
|
},
|
||||||
"Имя, логин и статус — как в профиле Telegram." : {
|
"Имя, логин и статус — как в профиле Telegram." : {
|
||||||
"comment" : "Message profile about description"
|
"comment" : "Message profile about description"
|
||||||
@ -1337,6 +1337,9 @@
|
|||||||
"Не удалось выполнить поиск." : {
|
"Не удалось выполнить поиск." : {
|
||||||
"comment" : "Search error fallback\nSearch service decoding error"
|
"comment" : "Search error fallback\nSearch service decoding error"
|
||||||
},
|
},
|
||||||
|
"Не удалось добавить контакт." : {
|
||||||
|
"comment" : "Contacts service add unexpected status"
|
||||||
|
},
|
||||||
"Не удалось заблокировать пользователя." : {
|
"Не удалось заблокировать пользователя." : {
|
||||||
"comment" : "Blocked users create unexpected status"
|
"comment" : "Blocked users create unexpected status"
|
||||||
},
|
},
|
||||||
@ -1441,6 +1444,9 @@
|
|||||||
"Не удалось определить пользователя для блокировки." : {
|
"Не удалось определить пользователя для блокировки." : {
|
||||||
"comment" : "Message profile missing user id error"
|
"comment" : "Message profile missing user id error"
|
||||||
},
|
},
|
||||||
|
"Не удалось определить пользователя для добавления." : {
|
||||||
|
"comment" : "Contact add invalid user id error"
|
||||||
|
},
|
||||||
"Не удалось открыть чат" : {
|
"Не удалось открыть чат" : {
|
||||||
"comment" : "Chat creation error title"
|
"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"
|
"comment" : "Avatar encoding error"
|
||||||
|
|||||||
@ -289,7 +289,9 @@ struct MessageProfileView: View {
|
|||||||
rowDivider
|
rowDivider
|
||||||
if let profile = currentChatProfile {
|
if let profile = currentChatProfile {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
ContactAddView(contact: ContactEditInfo(profile: profile))
|
ContactAddView(contact: ContactEditInfo(profile: profile)) { payload in
|
||||||
|
handleContactAdded(payload)
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
filledActionLabel(
|
filledActionLabel(
|
||||||
title: NSLocalizedString("Добавить в контакты", comment: "Message profile add to contacts title"),
|
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() {
|
private func handleEditContactTap() {
|
||||||
showPlaceholderAction(
|
showPlaceholderAction(
|
||||||
title: NSLocalizedString("Ошибка", comment: "Common error title"),
|
title: NSLocalizedString("Ошибка", comment: "Common error title"),
|
||||||
|
|||||||
@ -2,12 +2,18 @@ import SwiftUI
|
|||||||
|
|
||||||
struct ContactAddView: View {
|
struct ContactAddView: View {
|
||||||
let contact: ContactEditInfo
|
let contact: ContactEditInfo
|
||||||
|
let onContactAdded: ((ContactPayload) -> Void)?
|
||||||
|
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
private let contactsService = ContactsService()
|
||||||
|
|
||||||
@State private var displayName: String
|
@State private var displayName: String
|
||||||
@State private var activeAlert: ContactAddAlert?
|
@State private var activeAlert: ContactAddAlert?
|
||||||
|
@State private var isSaving = false
|
||||||
|
|
||||||
init(contact: ContactEditInfo) {
|
init(contact: ContactEditInfo, onContactAdded: ((ContactPayload) -> Void)? = nil) {
|
||||||
self.contact = contact
|
self.contact = contact
|
||||||
|
self.onContactAdded = onContactAdded
|
||||||
let initialName = contact.preferredName
|
let initialName = contact.preferredName
|
||||||
_displayName = State(initialValue: initialName)
|
_displayName = State(initialValue: initialName)
|
||||||
}
|
}
|
||||||
@ -18,18 +24,23 @@ struct ContactAddView: View {
|
|||||||
|
|
||||||
Section(header: Text(NSLocalizedString("Публичная информация", comment: "Contact add public info section title"))) {
|
Section(header: Text(NSLocalizedString("Публичная информация", comment: "Contact add public info section title"))) {
|
||||||
TextField(NSLocalizedString("Отображаемое имя", comment: "Display name field placeholder"), text: $displayName)
|
TextField(NSLocalizedString("Отображаемое имя", comment: "Display name field placeholder"), text: $displayName)
|
||||||
|
.disabled(isSaving)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(NSLocalizedString("Новый контакт", comment: "Contact add title"))
|
.navigationTitle(NSLocalizedString("Новый контакт", comment: "Contact add title"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .confirmationAction) {
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
|
if isSaving {
|
||||||
|
ProgressView()
|
||||||
|
} else {
|
||||||
Button(NSLocalizedString("Сохранить", comment: "Contact add save button")) {
|
Button(NSLocalizedString("Сохранить", comment: "Contact add save button")) {
|
||||||
handleSaveTap()
|
handleSaveTap()
|
||||||
}
|
}
|
||||||
.disabled(!hasChanges)
|
.disabled(!hasChanges)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.alert(item: $activeAlert) { item in
|
.alert(item: $activeAlert) { item in
|
||||||
Alert(
|
Alert(
|
||||||
title: Text(item.title),
|
title: Text(item.title),
|
||||||
@ -116,10 +127,46 @@ struct ContactAddView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func handleSaveTap() {
|
private func handleSaveTap() {
|
||||||
|
guard !isSaving else { return }
|
||||||
|
|
||||||
|
guard let userId = UUID(uuidString: contact.userId) else {
|
||||||
activeAlert = ContactAddAlert(
|
activeAlert = ContactAddAlert(
|
||||||
title: NSLocalizedString("Скоро", comment: "Common soon title"),
|
title: NSLocalizedString("Ошибка", comment: "Common error title"),
|
||||||
message: NSLocalizedString("Добавление контакта появится позже.", comment: "Contact add placeholder message")
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user