Compare commits
No commits in common. "b1d91128063c1c53145952f56f958f37ad019a2e" and "af89cea3fbd035bd980035bdd17d658cecbe2815" have entirely different histories.
b1d9112806
...
af89cea3fb
@ -434,7 +434,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = yobble/yobble.entitlements;
|
CODE_SIGN_ENTITLEMENTS = yobble/yobble.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 11;
|
CURRENT_PROJECT_VERSION = 9;
|
||||||
DEVELOPMENT_TEAM = V22H44W47J;
|
DEVELOPMENT_TEAM = V22H44W47J;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@ -475,7 +475,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = yobble/yobble.entitlements;
|
CODE_SIGN_ENTITLEMENTS = yobble/yobble.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 11;
|
CURRENT_PROJECT_VERSION = 9;
|
||||||
DEVELOPMENT_TEAM = V22H44W47J;
|
DEVELOPMENT_TEAM = V22H44W47J;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
|
|||||||
@ -331,20 +331,6 @@ 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,7 +3,6 @@ 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 {
|
||||||
@ -13,8 +12,6 @@ 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -33,35 +30,15 @@ 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?
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct ContactDeleteRequestPayload: Encodable {
|
|
||||||
let userId: UUID
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct ContactUpdateRequestPayload: Encodable {
|
|
||||||
let userId: UUID
|
|
||||||
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) {
|
||||||
@ -111,167 +88,6 @@ 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeContact(userId: UUID, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
||||||
let request = ContactDeleteRequestPayload(userId: userId)
|
|
||||||
|
|
||||||
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/remove",
|
|
||||||
method: .delete,
|
|
||||||
body: body,
|
|
||||||
requiresAuth: true
|
|
||||||
) { [decoder] result in
|
|
||||||
switch result {
|
|
||||||
case .success(let response):
|
|
||||||
do {
|
|
||||||
let apiResponse = try decoder.decode(APIResponse<MessagePayload>.self, from: response.data)
|
|
||||||
guard apiResponse.status == "fine" else {
|
|
||||||
let message = apiResponse.detail ?? NSLocalizedString("Не удалось удалить контакт.", comment: "Contacts service delete unexpected status")
|
|
||||||
completion(.failure(ContactsServiceError.unexpectedStatus(message)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
completion(.success(()))
|
|
||||||
} catch {
|
|
||||||
let debugMessage = Self.describeDecodingError(error: error, data: response.data)
|
|
||||||
if AppConfig.DEBUG {
|
|
||||||
print("[ContactsService] decode contact delete 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 removeContact(userId: UUID) async throws {
|
|
||||||
try await withCheckedThrowingContinuation { continuation in
|
|
||||||
removeContact(userId: userId) { result in
|
|
||||||
continuation.resume(with: result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateContact(userId: UUID, customName: String?, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
||||||
let request = ContactUpdateRequestPayload(userId: userId, 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/update",
|
|
||||||
method: .patch,
|
|
||||||
body: body,
|
|
||||||
requiresAuth: true
|
|
||||||
) { [decoder] result in
|
|
||||||
switch result {
|
|
||||||
case .success(let response):
|
|
||||||
do {
|
|
||||||
let apiResponse = try decoder.decode(APIResponse<MessagePayload>.self, from: response.data)
|
|
||||||
guard apiResponse.status == "fine" else {
|
|
||||||
let message = apiResponse.detail ?? NSLocalizedString("Не удалось обновить контакт.", comment: "Contacts service update unexpected status")
|
|
||||||
completion(.failure(ContactsServiceError.unexpectedStatus(message)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
completion(.success(()))
|
|
||||||
} catch {
|
|
||||||
let debugMessage = Self.describeDecodingError(error: error, data: response.data)
|
|
||||||
if AppConfig.DEBUG {
|
|
||||||
print("[ContactsService] decode contact update 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 updateContact(userId: UUID, customName: String?) async throws {
|
|
||||||
try await withCheckedThrowingContinuation { continuation in
|
|
||||||
updateContact(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)
|
||||||
|
|||||||
@ -809,28 +809,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Изменение фото недоступно" : {
|
|
||||||
"comment" : "Contact add avatar unavailable title\nContact edit avatar unavailable title",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Photo change unavailable"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Изменить" : {
|
|
||||||
"comment" : "Message profile edit contact button",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Edit"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Изменить контакт" : {
|
"Изменить контакт" : {
|
||||||
"comment" : "Contacts context action edit"
|
"comment" : "Contacts context action edit"
|
||||||
},
|
},
|
||||||
@ -838,27 +816,13 @@
|
|||||||
|
|
||||||
},
|
},
|
||||||
"Изменить фото" : {
|
"Изменить фото" : {
|
||||||
"comment" : "Edit avatar button title",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Change photo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"Изображение" : {
|
"Изображение" : {
|
||||||
"comment" : "Image message placeholder"
|
"comment" : "Image message placeholder"
|
||||||
},
|
},
|
||||||
"Имя в чате" : {
|
"Имя в чате" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Имя контакта должно быть короче 32 символов." : {
|
|
||||||
"comment" : "Contact edit name too long message"
|
|
||||||
},
|
|
||||||
"Имя не может быть пустым." : {
|
|
||||||
"comment" : "Contact add empty name error\nContact edit empty name error"
|
|
||||||
},
|
},
|
||||||
"Имя, логин и статус — как в профиле Telegram." : {
|
"Имя, логин и статус — как в профиле Telegram." : {
|
||||||
"comment" : "Message profile about description"
|
"comment" : "Message profile about description"
|
||||||
@ -930,20 +894,6 @@
|
|||||||
"Коды восстановления" : {
|
"Коды восстановления" : {
|
||||||
"comment" : "Раздел кодов восстановления 2FA"
|
"comment" : "Раздел кодов восстановления 2FA"
|
||||||
},
|
},
|
||||||
"Контакт" : {
|
|
||||||
"comment" : "Contact edit title",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Contact"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Контакт \"%1$@\" будет удалён из списка." : {
|
|
||||||
"comment" : "Contact delete confirmation message"
|
|
||||||
},
|
|
||||||
"Контактов пока нет" : {
|
"Контактов пока нет" : {
|
||||||
"comment" : "Contacts empty state title"
|
"comment" : "Contacts empty state title"
|
||||||
},
|
},
|
||||||
@ -1179,17 +1129,6 @@
|
|||||||
"Мы отправим письмо, как только функция будет готова." : {
|
"Мы отправим письмо, как только функция будет готова." : {
|
||||||
"comment" : "Сообщение при недоступной отправке письма"
|
"comment" : "Сообщение при недоступной отправке письма"
|
||||||
},
|
},
|
||||||
"Мы пока не можем обновить фото контакта." : {
|
|
||||||
"comment" : "Contact add avatar unavailable message\nContact edit avatar unavailable message",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "We can’t update the contact photo yet."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Мы постараемся всё исправить. Напишите, что смутило." : {
|
"Мы постараемся всё исправить. Напишите, что смутило." : {
|
||||||
"comment" : "feedback: rating description 2",
|
"comment" : "feedback: rating description 2",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -1343,9 +1282,6 @@
|
|||||||
"Не удалось выполнить поиск." : {
|
"Не удалось выполнить поиск." : {
|
||||||
"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"
|
||||||
},
|
},
|
||||||
@ -1355,17 +1291,6 @@
|
|||||||
"Не удалось завершить сессию." : {
|
"Не удалось завершить сессию." : {
|
||||||
"comment" : "Sessions service revoke unexpected status"
|
"comment" : "Sessions service revoke unexpected status"
|
||||||
},
|
},
|
||||||
"Не удалось загрузить данные контакта." : {
|
|
||||||
"comment" : "Contact edit missing profile message",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Failed to load contact data."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Не удалось загрузить историю чата." : {
|
"Не удалось загрузить историю чата." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
@ -1413,9 +1338,6 @@
|
|||||||
"Не удалось обновить аватар." : {
|
"Не удалось обновить аватар." : {
|
||||||
"comment" : "Avatar upload unexpected status"
|
"comment" : "Avatar upload unexpected status"
|
||||||
},
|
},
|
||||||
"Не удалось обновить контакт." : {
|
|
||||||
"comment" : "Contacts service update unexpected status"
|
|
||||||
},
|
|
||||||
"Не удалось обновить пароль." : {
|
"Не удалось обновить пароль." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -1450,15 +1372,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Не удалось определить контакт." : {
|
|
||||||
"comment" : "Contact delete invalid user id error\nContact edit invalid user id error"
|
|
||||||
},
|
|
||||||
"Не удалось определить пользователя для блокировки." : {
|
"Не удалось определить пользователя для блокировки." : {
|
||||||
"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"
|
||||||
},
|
},
|
||||||
@ -1472,7 +1388,7 @@
|
|||||||
|
|
||||||
},
|
},
|
||||||
"Не удалось подготовить данные запроса." : {
|
"Не удалось подготовить данные запроса." : {
|
||||||
"comment" : "Blocked users create encoding error\nBlocked users delete encoding error\nContacts service encoding error\nProfile update encoding error"
|
"comment" : "Blocked users create encoding error\nBlocked users delete encoding error\nProfile update encoding error"
|
||||||
},
|
},
|
||||||
"Не удалось подготовить изображение для загрузки." : {
|
"Не удалось подготовить изображение для загрузки." : {
|
||||||
"comment" : "Avatar encoding error"
|
"comment" : "Avatar encoding error"
|
||||||
@ -1496,9 +1412,6 @@
|
|||||||
"Не удалось сохранить изменения профиля." : {
|
"Не удалось сохранить изменения профиля." : {
|
||||||
"comment" : "Profile update unexpected status"
|
"comment" : "Profile update unexpected status"
|
||||||
},
|
},
|
||||||
"Не удалось удалить контакт." : {
|
|
||||||
"comment" : "Contacts service delete unexpected status"
|
|
||||||
},
|
|
||||||
"Не удалось удалить пользователя из списка." : {
|
"Не удалось удалить пользователя из списка." : {
|
||||||
"comment" : "Blocked users delete unexpected status"
|
"comment" : "Blocked users delete unexpected status"
|
||||||
},
|
},
|
||||||
@ -1685,9 +1598,6 @@
|
|||||||
"Новое сообщение" : {
|
"Новое сообщение" : {
|
||||||
"comment" : "Default banner subtitle"
|
"comment" : "Default banner subtitle"
|
||||||
},
|
},
|
||||||
"Новый контакт" : {
|
|
||||||
"comment" : "Contact add title"
|
|
||||||
},
|
|
||||||
"Новый пароль" : {
|
"Новый пароль" : {
|
||||||
"comment" : "Новый пароль",
|
"comment" : "Новый пароль",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -1802,7 +1712,7 @@
|
|||||||
"comment" : "Common cancel\nОбщий текст кнопки отмены"
|
"comment" : "Common cancel\nОбщий текст кнопки отмены"
|
||||||
},
|
},
|
||||||
"Отображаемое имя" : {
|
"Отображаемое имя" : {
|
||||||
"comment" : "Display name field placeholder"
|
|
||||||
},
|
},
|
||||||
"Отправить код ещё раз" : {
|
"Отправить код ещё раз" : {
|
||||||
|
|
||||||
@ -2486,7 +2396,7 @@
|
|||||||
|
|
||||||
},
|
},
|
||||||
"Публичная информация" : {
|
"Публичная информация" : {
|
||||||
"comment" : "Contact add public info section title\nProfile info section title"
|
|
||||||
},
|
},
|
||||||
"Публичное имя" : {
|
"Публичное имя" : {
|
||||||
|
|
||||||
@ -2603,18 +2513,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Редактирование контакта появится позже." : {
|
|
||||||
"comment" : "Message profile edit contact alert message",
|
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Contact editing will be available later."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Редактировать профиль" : {
|
"Редактировать профиль" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -2761,9 +2659,6 @@
|
|||||||
},
|
},
|
||||||
"Скрыть" : {
|
"Скрыть" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Слишком длинное имя" : {
|
|
||||||
"comment" : "Contact edit name too long title"
|
|
||||||
},
|
},
|
||||||
"Слишком много запросов." : {
|
"Слишком много запросов." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -2836,17 +2731,6 @@
|
|||||||
"Сохраните секретный ключ и введите код из приложения, чтобы завершить настройку." : {
|
"Сохраните секретный ключ и введите код из приложения, чтобы завершить настройку." : {
|
||||||
"comment" : "Сообщение после активации 2FA"
|
"comment" : "Сообщение после активации 2FA"
|
||||||
},
|
},
|
||||||
"Сохранить" : {
|
|
||||||
"comment" : "Contact add save button\nContact edit save button",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Save"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Сохранить изменения" : {
|
"Сохранить изменения" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -3015,29 +2899,11 @@
|
|||||||
"Удаление контакта \"%1$@\" появится позже." : {
|
"Удаление контакта \"%1$@\" появится позже." : {
|
||||||
"comment" : "Contacts delete placeholder message"
|
"comment" : "Contacts delete placeholder message"
|
||||||
},
|
},
|
||||||
"Удаление контакта появится позже." : {
|
|
||||||
"comment" : "Contact edit delete placeholder message",
|
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Contact deletion will be available later."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Удалить" : {
|
|
||||||
"comment" : "Contact delete confirm action"
|
|
||||||
},
|
|
||||||
"Удалить из заблокированных?" : {
|
"Удалить из заблокированных?" : {
|
||||||
"comment" : "Unblock confirmation title"
|
"comment" : "Unblock confirmation title"
|
||||||
},
|
},
|
||||||
"Удалить контакт" : {
|
"Удалить контакт" : {
|
||||||
"comment" : "Contact edit delete action\nContacts context action delete"
|
"comment" : "Contacts context action delete"
|
||||||
},
|
|
||||||
"Удалить контакт?" : {
|
|
||||||
"comment" : "Contact delete confirmation title"
|
|
||||||
},
|
},
|
||||||
"Удалить фото" : {
|
"Удалить фото" : {
|
||||||
"comment" : "Avatar delete"
|
"comment" : "Avatar delete"
|
||||||
@ -3052,9 +2918,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Удаляем..." : {
|
|
||||||
"comment" : "Contact delete in progress"
|
|
||||||
},
|
|
||||||
"Удалять аккаунт через %lld дн." : {
|
"Удалять аккаунт через %lld дн." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|||||||
@ -43,31 +43,6 @@ struct MessageProfileView: View {
|
|||||||
.background(Color(UIColor.systemGroupedBackground).ignoresSafeArea())
|
.background(Color(UIColor.systemGroupedBackground).ignoresSafeArea())
|
||||||
.navigationTitle(NSLocalizedString("Профиль", comment: "Message profile navigation title"))
|
.navigationTitle(NSLocalizedString("Профиль", comment: "Message profile navigation title"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
|
||||||
if canEditContact {
|
|
||||||
if let profile = currentChatProfile {
|
|
||||||
NavigationLink {
|
|
||||||
ContactEditView(
|
|
||||||
contact: ContactEditInfo(profile: profile),
|
|
||||||
onContactDeleted: {
|
|
||||||
handleContactDeleted()
|
|
||||||
},
|
|
||||||
onContactUpdated: { newName in
|
|
||||||
handleContactUpdated(newName)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} label: {
|
|
||||||
Text(NSLocalizedString("Изменить", comment: "Message profile edit contact button"))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Button(NSLocalizedString("Изменить", comment: "Message profile edit contact button")) {
|
|
||||||
handleEditContactTap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.alert(item: $placeholderAlert) { alert in
|
.alert(item: $placeholderAlert) { alert in
|
||||||
Alert(
|
Alert(
|
||||||
title: Text(alert.title),
|
title: Text(alert.title),
|
||||||
@ -295,25 +270,11 @@ struct MessageProfileView: View {
|
|||||||
|
|
||||||
if shouldShowRelationshipQuickActions {
|
if shouldShowRelationshipQuickActions {
|
||||||
rowDivider
|
rowDivider
|
||||||
if let profile = currentChatProfile {
|
filledActionButton(
|
||||||
NavigationLink {
|
title: NSLocalizedString("Добавить в контакты", comment: "Message profile add to contacts title"),
|
||||||
ContactAddView(contact: ContactEditInfo(profile: profile)) { payload in
|
tint: Color.accentColor
|
||||||
handleContactAdded(payload)
|
) {
|
||||||
}
|
handleAddContactTap()
|
||||||
} label: {
|
|
||||||
filledActionLabel(
|
|
||||||
title: NSLocalizedString("Добавить в контакты", comment: "Message profile add to contacts title"),
|
|
||||||
tint: Color.accentColor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
} else {
|
|
||||||
filledActionButton(
|
|
||||||
title: NSLocalizedString("Добавить в контакты", comment: "Message profile add to contacts title"),
|
|
||||||
tint: Color.accentColor
|
|
||||||
) {
|
|
||||||
handleAddContactTap()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rowDivider
|
rowDivider
|
||||||
@ -523,31 +484,23 @@ struct MessageProfileView: View {
|
|||||||
action: @escaping () -> Void
|
action: @escaping () -> Void
|
||||||
) -> some View {
|
) -> some View {
|
||||||
Button(action: action) {
|
Button(action: action) {
|
||||||
filledActionLabel(title: title, subtitle: subtitle, tint: tint)
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(title)
|
||||||
|
.font(.body)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
if let subtitle {
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(tint.opacity(0.7))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundColor(tint)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.vertical, 10)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func filledActionLabel(
|
|
||||||
title: String,
|
|
||||||
subtitle: String? = nil,
|
|
||||||
tint: Color
|
|
||||||
) -> some View {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text(title)
|
|
||||||
.font(.body)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
if let subtitle {
|
|
||||||
Text(subtitle)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(tint.opacity(0.7))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.foregroundColor(tint)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(.vertical, 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func iconBackground<Content: View>(color: Color, @ViewBuilder content: () -> Content) -> some View {
|
private func iconBackground<Content: View>(color: Color, @ViewBuilder content: () -> Content) -> some View {
|
||||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||||
.fill(color)
|
.fill(color)
|
||||||
@ -573,98 +526,6 @@ 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 handleContactUpdated(_ newName: String) {
|
|
||||||
guard let profile = currentChatProfile else { return }
|
|
||||||
|
|
||||||
let updatedProfile = ChatProfile(
|
|
||||||
userId: profile.userId,
|
|
||||||
login: profile.login,
|
|
||||||
fullName: profile.fullName,
|
|
||||||
customName: newName,
|
|
||||||
bio: profile.bio,
|
|
||||||
lastSeen: profile.lastSeen,
|
|
||||||
createdAt: profile.createdAt,
|
|
||||||
avatars: profile.avatars,
|
|
||||||
stories: profile.stories,
|
|
||||||
permissions: profile.permissions,
|
|
||||||
profilePermissions: profile.profilePermissions,
|
|
||||||
relationship: profile.relationship,
|
|
||||||
rating: profile.rating,
|
|
||||||
isOfficial: profile.isOfficial
|
|
||||||
)
|
|
||||||
|
|
||||||
chatProfile = updatedProfile
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleContactDeleted() {
|
|
||||||
guard let profile = currentChatProfile else { return }
|
|
||||||
|
|
||||||
let existingRelationship = profile.relationship
|
|
||||||
let updatedRelationship = RelationshipStatus(
|
|
||||||
isTargetInContactsOfCurrentUser: false,
|
|
||||||
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: nil,
|
|
||||||
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"),
|
|
||||||
message: NSLocalizedString("Не удалось загрузить данные контакта.", comment: "Contact edit missing profile message")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleBlockToggleTap() {
|
private func handleBlockToggleTap() {
|
||||||
guard !isProcessingBlockAction else { return }
|
guard !isProcessingBlockAction else { return }
|
||||||
|
|
||||||
@ -965,10 +826,6 @@ struct MessageProfileView: View {
|
|||||||
chatProfile ?? chat.chatData
|
chatProfile ?? chat.chatData
|
||||||
}
|
}
|
||||||
|
|
||||||
private var canEditContact: Bool {
|
|
||||||
currentChatProfile?.relationship?.isTargetInContactsOfCurrentUser ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
private var isBlockedByCurrentUser: Bool { isBlockedByCurrentUserState }
|
private var isBlockedByCurrentUser: Bool { isBlockedByCurrentUserState }
|
||||||
|
|
||||||
private var avatarUrl: URL? {
|
private var avatarUrl: URL? {
|
||||||
|
|||||||
@ -1,184 +0,0 @@
|
|||||||
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, onContactAdded: ((ContactPayload) -> Void)? = nil) {
|
|
||||||
self.contact = contact
|
|
||||||
self.onContactAdded = onContactAdded
|
|
||||||
let initialName = contact.preferredName
|
|
||||||
_displayName = State(initialValue: initialName)
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Form {
|
|
||||||
avatarSection
|
|
||||||
|
|
||||||
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),
|
|
||||||
message: Text(item.message),
|
|
||||||
dismissButton: .default(Text(NSLocalizedString("Понятно", comment: "Placeholder alert dismiss")))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var avatarSection: some View {
|
|
||||||
Section {
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
avatarView
|
|
||||||
.frame(width: 120, height: 120)
|
|
||||||
.clipShape(Circle())
|
|
||||||
|
|
||||||
Button(NSLocalizedString("Изменить фото", comment: "Edit avatar button title")) {
|
|
||||||
showAvatarUnavailableAlert()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.listRowBackground(Color(UIColor.systemGroupedBackground))
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var avatarView: some View {
|
|
||||||
if let url = avatarURL,
|
|
||||||
let fileId = contact.avatarFileId {
|
|
||||||
CachedAvatarView(url: url, fileId: fileId, userId: contact.userId) {
|
|
||||||
avatarPlaceholder
|
|
||||||
}
|
|
||||||
.aspectRatio(contentMode: .fill)
|
|
||||||
} else {
|
|
||||||
avatarPlaceholder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var avatarPlaceholder: some View {
|
|
||||||
Circle()
|
|
||||||
.fill(Color.accentColor.opacity(0.15))
|
|
||||||
.overlay(
|
|
||||||
Text(avatarInitial)
|
|
||||||
.font(.system(size: 48, weight: .semibold))
|
|
||||||
.foregroundColor(.accentColor)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var avatarInitial: String {
|
|
||||||
let trimmedName = displayName.trimmedNonEmpty ?? contact.preferredName
|
|
||||||
if let first = trimmedName.trimmingCharacters(in: .whitespacesAndNewlines).first {
|
|
||||||
return String(first).uppercased()
|
|
||||||
}
|
|
||||||
if let login = contact.login?.trimmingCharacters(in: .whitespacesAndNewlines), !login.isEmpty {
|
|
||||||
return String(login.prefix(1)).uppercased()
|
|
||||||
}
|
|
||||||
return "?"
|
|
||||||
}
|
|
||||||
|
|
||||||
private var avatarURL: URL? {
|
|
||||||
guard let fileId = contact.avatarFileId else { return nil }
|
|
||||||
return URL(string: "\(AppConfig.API_SERVER)/v1/storage/download/avatar/\(contact.userId)?file_id=\(fileId)")
|
|
||||||
}
|
|
||||||
|
|
||||||
private var hasChanges: Bool {
|
|
||||||
let trimmed = displayName.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
guard !trimmed.isEmpty else { return false }
|
|
||||||
|
|
||||||
if let existing = contact.customName?.trimmedNonEmpty {
|
|
||||||
return trimmed != existing
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private func showAvatarUnavailableAlert() {
|
|
||||||
activeAlert = ContactAddAlert(
|
|
||||||
title: NSLocalizedString("Изменение фото недоступно", comment: "Contact add avatar unavailable title"),
|
|
||||||
message: NSLocalizedString("Мы пока не можем обновить фото контакта.", comment: "Contact add avatar unavailable message")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleSaveTap() {
|
|
||||||
guard !isSaving else { return }
|
|
||||||
|
|
||||||
guard let userId = UUID(uuidString: contact.userId) else {
|
|
||||||
activeAlert = ContactAddAlert(
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct ContactAddAlert: Identifiable {
|
|
||||||
let id = UUID()
|
|
||||||
let title: String
|
|
||||||
let message: String
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension String {
|
|
||||||
var trimmedNonEmpty: String? {
|
|
||||||
let value = trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
return value.isEmpty ? nil : value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,330 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ContactEditInfo {
|
|
||||||
let userId: String
|
|
||||||
let login: String?
|
|
||||||
let fullName: String?
|
|
||||||
let customName: String?
|
|
||||||
let avatarFileId: String?
|
|
||||||
|
|
||||||
init(userId: String, login: String?, fullName: String?, customName: String?, avatarFileId: String?) {
|
|
||||||
self.userId = userId
|
|
||||||
self.login = login
|
|
||||||
self.fullName = fullName
|
|
||||||
self.customName = customName
|
|
||||||
self.avatarFileId = avatarFileId
|
|
||||||
}
|
|
||||||
|
|
||||||
init(userId: UUID, login: String?, fullName: String?, customName: String?, avatarFileId: String?) {
|
|
||||||
self.init(userId: userId.uuidString, login: login, fullName: fullName, customName: customName, avatarFileId: avatarFileId)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(profile: ChatProfile) {
|
|
||||||
self.init(
|
|
||||||
userId: profile.userId,
|
|
||||||
login: profile.login,
|
|
||||||
fullName: profile.fullName,
|
|
||||||
customName: profile.customName,
|
|
||||||
avatarFileId: profile.avatars?.current?.fileId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(payload: ContactPayload) {
|
|
||||||
self.init(
|
|
||||||
userId: payload.userId,
|
|
||||||
login: payload.login,
|
|
||||||
fullName: payload.fullName,
|
|
||||||
customName: payload.customName,
|
|
||||||
avatarFileId: nil
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var preferredName: String {
|
|
||||||
if let custom = customName?.trimmedNonEmpty {
|
|
||||||
return custom
|
|
||||||
}
|
|
||||||
if let full = fullName?.trimmedNonEmpty {
|
|
||||||
return full
|
|
||||||
}
|
|
||||||
if let login, !login.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
||||||
return "@\(login)"
|
|
||||||
}
|
|
||||||
return NSLocalizedString("Неизвестный пользователь", comment: "Message profile fallback title")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ContactEditView: View {
|
|
||||||
let contact: ContactEditInfo
|
|
||||||
let onContactDeleted: (() -> Void)?
|
|
||||||
let onContactUpdated: ((String) -> Void)?
|
|
||||||
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
private let contactsService = ContactsService()
|
|
||||||
|
|
||||||
@State private var displayName: String
|
|
||||||
@State private var activeAlert: ContactEditAlert?
|
|
||||||
@State private var isSaving = false
|
|
||||||
@State private var isDeleting = false
|
|
||||||
@State private var showDeleteConfirmation = false
|
|
||||||
|
|
||||||
init(
|
|
||||||
contact: ContactEditInfo,
|
|
||||||
onContactDeleted: (() -> Void)? = nil,
|
|
||||||
onContactUpdated: ((String) -> Void)? = nil
|
|
||||||
) {
|
|
||||||
self.contact = contact
|
|
||||||
self.onContactDeleted = onContactDeleted
|
|
||||||
self.onContactUpdated = onContactUpdated
|
|
||||||
let initialName = contact.preferredName
|
|
||||||
_displayName = State(initialValue: initialName)
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Form {
|
|
||||||
avatarSection
|
|
||||||
|
|
||||||
Section(header: Text(NSLocalizedString("Публичная информация", comment: "Profile info section title"))) {
|
|
||||||
TextField(NSLocalizedString("Отображаемое имя", comment: "Display name field placeholder"), text: $displayName)
|
|
||||||
.disabled(isSaving || isDeleting)
|
|
||||||
}
|
|
||||||
|
|
||||||
Section {
|
|
||||||
Button(role: .destructive) {
|
|
||||||
handleDeleteTap()
|
|
||||||
} label: {
|
|
||||||
deleteButtonLabel
|
|
||||||
}
|
|
||||||
.disabled(isDeleting)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationTitle(NSLocalizedString("Контакт", comment: "Contact edit title"))
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .confirmationAction) {
|
|
||||||
if isSaving {
|
|
||||||
ProgressView()
|
|
||||||
} else {
|
|
||||||
Button(NSLocalizedString("Сохранить", comment: "Contact edit save button")) {
|
|
||||||
handleSaveTap()
|
|
||||||
}
|
|
||||||
.disabled(!hasChanges || isDeleting)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.alert(item: $activeAlert) { item in
|
|
||||||
Alert(
|
|
||||||
title: Text(item.title),
|
|
||||||
message: Text(item.message),
|
|
||||||
dismissButton: .default(Text(NSLocalizedString("Понятно", comment: "Placeholder alert dismiss")))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.confirmationDialog(
|
|
||||||
NSLocalizedString("Удалить контакт?", comment: "Contact delete confirmation title"),
|
|
||||||
isPresented: $showDeleteConfirmation,
|
|
||||||
titleVisibility: .visible
|
|
||||||
) {
|
|
||||||
Button(NSLocalizedString("Удалить", comment: "Contact delete confirm action"), role: .destructive) {
|
|
||||||
confirmDelete()
|
|
||||||
}
|
|
||||||
Button(NSLocalizedString("Отмена", comment: "Common cancel"), role: .cancel) {
|
|
||||||
showDeleteConfirmation = false
|
|
||||||
}
|
|
||||||
} message: {
|
|
||||||
Text(String(
|
|
||||||
format: NSLocalizedString("Контакт \"%1$@\" будет удалён из списка.", comment: "Contact delete confirmation message"),
|
|
||||||
contact.preferredName
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var deleteButtonLabel: some View {
|
|
||||||
if isDeleting {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
ProgressView()
|
|
||||||
Text(NSLocalizedString("Удаляем...", comment: "Contact delete in progress"))
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
|
||||||
} else {
|
|
||||||
Text(NSLocalizedString("Удалить контакт", comment: "Contact edit delete action"))
|
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var avatarSection: some View {
|
|
||||||
Section {
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
avatarView
|
|
||||||
.frame(width: 120, height: 120)
|
|
||||||
.clipShape(Circle())
|
|
||||||
|
|
||||||
Button(NSLocalizedString("Изменить фото", comment: "Edit avatar button title")) {
|
|
||||||
showAvatarUnavailableAlert()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.listRowBackground(Color(UIColor.systemGroupedBackground))
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var avatarView: some View {
|
|
||||||
if let url = avatarURL,
|
|
||||||
let fileId = contact.avatarFileId {
|
|
||||||
CachedAvatarView(url: url, fileId: fileId, userId: contact.userId) {
|
|
||||||
avatarPlaceholder
|
|
||||||
}
|
|
||||||
.aspectRatio(contentMode: .fill)
|
|
||||||
} else {
|
|
||||||
avatarPlaceholder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var avatarPlaceholder: some View {
|
|
||||||
Circle()
|
|
||||||
.fill(Color.accentColor.opacity(0.15))
|
|
||||||
.overlay(
|
|
||||||
Text(avatarInitial)
|
|
||||||
.font(.system(size: 48, weight: .semibold))
|
|
||||||
.foregroundColor(.accentColor)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var avatarInitial: String {
|
|
||||||
let trimmedName = displayName.trimmedNonEmpty ?? contact.preferredName
|
|
||||||
if let first = trimmedName.trimmingCharacters(in: .whitespacesAndNewlines).first {
|
|
||||||
return String(first).uppercased()
|
|
||||||
}
|
|
||||||
if let login = contact.login?.trimmingCharacters(in: .whitespacesAndNewlines), !login.isEmpty {
|
|
||||||
return String(login.prefix(1)).uppercased()
|
|
||||||
}
|
|
||||||
return "?"
|
|
||||||
}
|
|
||||||
|
|
||||||
private var avatarURL: URL? {
|
|
||||||
guard let fileId = contact.avatarFileId else { return nil }
|
|
||||||
return URL(string: "\(AppConfig.API_SERVER)/v1/storage/download/avatar/\(contact.userId)?file_id=\(fileId)")
|
|
||||||
}
|
|
||||||
|
|
||||||
private var hasChanges: Bool {
|
|
||||||
let trimmed = displayName.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
guard !trimmed.isEmpty else { return false }
|
|
||||||
|
|
||||||
if let existing = contact.customName?.trimmedNonEmpty {
|
|
||||||
return trimmed != existing
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private func showAvatarUnavailableAlert() {
|
|
||||||
activeAlert = ContactEditAlert(
|
|
||||||
title: NSLocalizedString("Изменение фото недоступно", comment: "Contact edit avatar unavailable title"),
|
|
||||||
message: NSLocalizedString("Мы пока не можем обновить фото контакта.", comment: "Contact edit avatar unavailable message")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleSaveTap() {
|
|
||||||
guard !isSaving, !isDeleting else { return }
|
|
||||||
|
|
||||||
guard let userId = UUID(uuidString: contact.userId) else {
|
|
||||||
activeAlert = ContactEditAlert(
|
|
||||||
title: NSLocalizedString("Ошибка", comment: "Common error title"),
|
|
||||||
message: NSLocalizedString("Не удалось определить контакт.", comment: "Contact edit invalid user id error")
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let trimmed = displayName.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
guard !trimmed.isEmpty else {
|
|
||||||
activeAlert = ContactEditAlert(
|
|
||||||
title: NSLocalizedString("Ошибка", comment: "Common error title"),
|
|
||||||
message: NSLocalizedString("Имя не может быть пустым.", comment: "Contact edit empty name error")
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if trimmed.count > 32 {
|
|
||||||
activeAlert = ContactEditAlert(
|
|
||||||
title: NSLocalizedString("Слишком длинное имя", comment: "Contact edit name too long title"),
|
|
||||||
message: NSLocalizedString("Имя контакта должно быть короче 32 символов.", comment: "Contact edit name too long message")
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isSaving = true
|
|
||||||
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
try await contactsService.updateContact(userId: userId, customName: trimmed)
|
|
||||||
await MainActor.run {
|
|
||||||
isSaving = false
|
|
||||||
onContactUpdated?(trimmed)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
await MainActor.run {
|
|
||||||
isSaving = false
|
|
||||||
activeAlert = ContactEditAlert(
|
|
||||||
title: NSLocalizedString("Ошибка", comment: "Common error title"),
|
|
||||||
message: error.localizedDescription
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleDeleteTap() {
|
|
||||||
guard !isDeleting else { return }
|
|
||||||
showDeleteConfirmation = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private func confirmDelete() {
|
|
||||||
guard !isDeleting else { return }
|
|
||||||
guard let userId = UUID(uuidString: contact.userId) else {
|
|
||||||
activeAlert = ContactEditAlert(
|
|
||||||
title: NSLocalizedString("Ошибка", comment: "Common error title"),
|
|
||||||
message: NSLocalizedString("Не удалось определить контакт.", comment: "Contact delete invalid user id error")
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isDeleting = true
|
|
||||||
showDeleteConfirmation = false
|
|
||||||
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
try await contactsService.removeContact(userId: userId)
|
|
||||||
await MainActor.run {
|
|
||||||
isDeleting = false
|
|
||||||
onContactDeleted?()
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
await MainActor.run {
|
|
||||||
isDeleting = false
|
|
||||||
activeAlert = ContactEditAlert(
|
|
||||||
title: NSLocalizedString("Ошибка", comment: "Common error title"),
|
|
||||||
message: error.localizedDescription
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct ContactEditAlert: Identifiable {
|
|
||||||
let id = UUID()
|
|
||||||
let title: String
|
|
||||||
let message: String
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension String {
|
|
||||||
var trimmedNonEmpty: String? {
|
|
||||||
let value = trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
return value.isEmpty ? nil : value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -70,27 +70,35 @@ struct EditPrivacyView: View {
|
|||||||
Toggle(NSLocalizedString("Показывать био не-контактам", comment: ""), isOn: $profilePermissions.showBioToNonContacts)
|
Toggle(NSLocalizedString("Показывать био не-контактам", comment: ""), isOn: $profilePermissions.showBioToNonContacts)
|
||||||
Toggle(NSLocalizedString("Показывать сторисы не-контактам", comment: ""), isOn: $profilePermissions.showStoriesToNonContacts)
|
Toggle(NSLocalizedString("Показывать сторисы не-контактам", comment: ""), isOn: $profilePermissions.showStoriesToNonContacts)
|
||||||
|
|
||||||
privacyScopePicker(
|
Picker(NSLocalizedString("Видимость статуса 'был в сети'", comment: ""), selection: $profilePermissions.lastSeenVisibility) {
|
||||||
title: NSLocalizedString("Видимость статуса 'был в сети'", comment: ""),
|
ForEach(privacyScopeOptions) { scope in
|
||||||
selection: $profilePermissions.lastSeenVisibility
|
Text(scope.title).tag(scope.rawValue)
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Text(NSLocalizedString("Приглашения и звонки", comment: ""))) {
|
Section(header: Text(NSLocalizedString("Приглашения и звонки", comment: ""))) {
|
||||||
privacyScopePicker(
|
Picker(NSLocalizedString("Кто может приглашать в паблики", comment: ""), selection: $profilePermissions.publicInvitePermission) {
|
||||||
title: NSLocalizedString("Кто может приглашать в паблики", comment: ""),
|
ForEach(privacyScopeOptions) { scope in
|
||||||
selection: $profilePermissions.publicInvitePermission
|
Text(scope.title).tag(scope.rawValue)
|
||||||
)
|
}
|
||||||
|
}
|
||||||
privacyScopePicker(
|
.pickerStyle(.segmented)
|
||||||
title: NSLocalizedString("Кто может приглашать в беседы", comment: ""),
|
|
||||||
selection: $profilePermissions.groupInvitePermission
|
Picker(NSLocalizedString("Кто может приглашать в беседы", comment: ""), selection: $profilePermissions.groupInvitePermission) {
|
||||||
)
|
ForEach(privacyScopeOptions) { scope in
|
||||||
|
Text(scope.title).tag(scope.rawValue)
|
||||||
privacyScopePicker(
|
}
|
||||||
title: NSLocalizedString("Кто может звонить", comment: ""),
|
}
|
||||||
selection: $profilePermissions.callPermission
|
.pickerStyle(.segmented)
|
||||||
)
|
|
||||||
|
Picker(NSLocalizedString("Кто может звонить", comment: ""), selection: $profilePermissions.callPermission) {
|
||||||
|
ForEach(privacyScopeOptions) { scope in
|
||||||
|
Text(scope.title).tag(scope.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Text(NSLocalizedString("Чаты и хранение", comment: ""))) {
|
Section(header: Text(NSLocalizedString("Чаты и хранение", comment: ""))) {
|
||||||
@ -184,24 +192,6 @@ struct EditPrivacyView: View {
|
|||||||
return "\(secondsString) (≈ \(formattedHours) ч.)"
|
return "\(secondsString) (≈ \(formattedHours) ч.)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func privacyScopePicker(title: String, selection: Binding<Int>) -> some View {
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
|
||||||
Text(title)
|
|
||||||
.font(.body)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
Picker("", selection: selection) {
|
|
||||||
ForEach(privacyScopeOptions) { scope in
|
|
||||||
Text(scope.title).tag(scope.rawValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pickerStyle(.segmented)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum PrivacyScope: Int, CaseIterable, Identifiable {
|
private enum PrivacyScope: Int, CaseIterable, Identifiable {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user