From fb8413e68c51eddefe25c4b2fd6cba510a253801 Mon Sep 17 00:00:00 2001 From: cheykrym Date: Wed, 8 Oct 2025 02:09:44 +0300 Subject: [PATCH] add edit privacy --- yobble/Network/ProfileModels.swift | 21 ++++ yobble/Network/ProfileService.swift | 63 ++++++++++++ yobble/Resources/Localizable.xcstrings | 15 +++ .../Views/Tab/Settings/EditPrivacyView.swift | 98 ++++++++++++++++++- 4 files changed, 193 insertions(+), 4 deletions(-) diff --git a/yobble/Network/ProfileModels.swift b/yobble/Network/ProfileModels.swift index 4537425..8d9cc70 100644 --- a/yobble/Network/ProfileModels.swift +++ b/yobble/Network/ProfileModels.swift @@ -94,3 +94,24 @@ struct ProfilePermissionsPayload: Decodable { let maxMessageAutoDeleteSeconds: Int? let autoDeleteAfterDays: Int? } + +struct ProfilePermissionsRequestPayload: Encodable { + let isSearchable: Bool + let allowMessageForwarding: Bool + let allowMessagesFromNonContacts: Bool + let showProfilePhotoToNonContacts: Bool + let lastSeenVisibility: Int + let showBioToNonContacts: Bool + let showStoriesToNonContacts: Bool + let allowServerChats: Bool + let publicInvitePermission: Int + let groupInvitePermission: Int + let callPermission: Int + let forceAutoDeleteMessagesInPrivate: Bool + let maxMessageAutoDeleteSeconds: Int? + let autoDeleteAfterDays: Int? +} + +struct ProfileUpdateRequestPayload: Encodable { + let profilePermissions: ProfilePermissionsRequestPayload +} diff --git a/yobble/Network/ProfileService.swift b/yobble/Network/ProfileService.swift index d16a68f..19e8bbb 100644 --- a/yobble/Network/ProfileService.swift +++ b/yobble/Network/ProfileService.swift @@ -3,6 +3,7 @@ import Foundation enum ProfileServiceError: LocalizedError { case unexpectedStatus(String) case decoding(debugDescription: String) + case encoding(String) var errorDescription: String? { switch self { @@ -12,6 +13,8 @@ enum ProfileServiceError: LocalizedError { return AppConfig.DEBUG ? debugDescription : NSLocalizedString("Не удалось загрузить профиль.", comment: "Profile service decoding error") + case .encoding(let message): + return message } } } @@ -70,6 +73,66 @@ final class ProfileService { } } + func updateProfile(_ payload: ProfileUpdateRequestPayload, completion: @escaping (Result) -> Void) { + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + + guard let body = try? encoder.encode(payload) else { + let message = NSLocalizedString("Не удалось подготовить данные запроса.", comment: "Profile update encoding error") + completion(.failure(ProfileServiceError.encoding(message))) + return + } + + client.request( + path: "/v1/profile/edit", + method: .put, + body: body, + requiresAuth: true + ) { result in + switch result { + case .success(let response): + do { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let apiResponse = try decoder.decode(APIResponse.self, from: response.data) + guard apiResponse.status == "fine" else { + let message = apiResponse.detail ?? NSLocalizedString("Не удалось сохранить изменения профиля.", comment: "Profile update unexpected status") + completion(.failure(ProfileServiceError.unexpectedStatus(message))) + return + } + completion(.success(apiResponse.data.message)) + } catch { + let debugMessage = Self.describeDecodingError(error: error, data: response.data) + if AppConfig.DEBUG { + print("[ProfileService] decode update response failed: \(debugMessage)") + } + if AppConfig.DEBUG { + completion(.failure(ProfileServiceError.decoding(debugDescription: debugMessage))) + } else { + let message = NSLocalizedString("Не удалось обработать ответ сервера.", comment: "Profile update decode error") + completion(.failure(ProfileServiceError.unexpectedStatus(message))) + } + } + case .failure(let error): + if case let NetworkError.server(_, data) = error, + let data, + let message = Self.errorMessage(from: data) { + completion(.failure(ProfileServiceError.unexpectedStatus(message))) + return + } + completion(.failure(error)) + } + } + } + + func updateProfile(_ payload: ProfileUpdateRequestPayload) async throws -> String { + try await withCheckedThrowingContinuation { continuation in + updateProfile(payload) { 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) diff --git a/yobble/Resources/Localizable.xcstrings b/yobble/Resources/Localizable.xcstrings index 42f57ae..b232543 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -74,6 +74,7 @@ } }, "OK" : { + "comment" : "Profile update alert button", "localizations" : { "en" : { "stringUnit" : { @@ -219,6 +220,9 @@ "Глобальный поиск" : { "comment" : "Global search section" }, + "Готово" : { + "comment" : "Profile update success title" + }, "Данные" : { }, @@ -570,6 +574,9 @@ }, "Настройки приватности" : { + }, + "Настройки приватности обновлены." : { + "comment" : "Profile update success fallback" }, "Не удалось выполнить поиск." : { "comment" : "Search error fallback\nSearch service decoding error" @@ -621,6 +628,7 @@ } }, "Не удалось обработать ответ сервера." : { + "comment" : "Profile update decode error", "localizations" : { "en" : { "stringUnit" : { @@ -630,6 +638,9 @@ } } }, + "Не удалось подготовить данные запроса." : { + "comment" : "Profile update encoding error" + }, "Не удалось сериализовать данные запроса." : { "localizations" : { "en" : { @@ -640,6 +651,9 @@ } } }, + "Не удалось сохранить изменения профиля." : { + "comment" : "Profile update unexpected status" + }, "Неверный запрос (400)." : { "localizations" : { "en" : { @@ -823,6 +837,7 @@ }, "Ошибка" : { + "comment" : "Profile update error title", "localizations" : { "en" : { "stringUnit" : { diff --git a/yobble/Views/Tab/Settings/EditPrivacyView.swift b/yobble/Views/Tab/Settings/EditPrivacyView.swift index 048e0ec..aaa39d9 100644 --- a/yobble/Views/Tab/Settings/EditPrivacyView.swift +++ b/yobble/Views/Tab/Settings/EditPrivacyView.swift @@ -4,6 +4,8 @@ struct EditPrivacyView: View { @State private var profilePermissions = ProfilePermissionsState() @State private var isLoading = false @State private var loadError: String? + @State private var isSaving = false + @State private var alertData: AlertData? private let profileService = ProfileService() @@ -112,11 +114,20 @@ struct EditPrivacyView: View { } Section { - Button("Сохранить изменения") { - print("Параметры приватности: \(profilePermissions)") + Button { + Task { + await saveProfile() + } + } label: { + if isSaving { + ProgressView() + .frame(maxWidth: .infinity, alignment: .center) + } else { + Text("Сохранить изменения") + .frame(maxWidth: .infinity, alignment: .center) + } } - .frame(maxWidth: .infinity, alignment: .center) - .disabled(isLoading) + .disabled(isLoading || isSaving) } Section { @@ -130,6 +141,17 @@ struct EditPrivacyView: View { } } } + .alert(item: $alertData) { data in + Alert( + title: Text(data.kind == .success + ? NSLocalizedString("Готово", comment: "Profile update success title") + : NSLocalizedString("Ошибка", comment: "Profile update error title")), + message: Text(data.message), + dismissButton: .default(Text(NSLocalizedString("OK", comment: "Profile update alert button"))) { + alertData = nil + } + ) + } .navigationTitle("Настройки приватности") .onChange(of: profilePermissions.forceAutoDeleteMessagesInPrivate) { newValue in if newValue { @@ -212,7 +234,64 @@ extension ProfilePermissionsState { } } +private extension ProfilePermissionsState { + var requestPayload: ProfilePermissionsRequestPayload { + ProfilePermissionsRequestPayload( + isSearchable: isSearchable, + allowMessageForwarding: allowMessageForwarding, + allowMessagesFromNonContacts: allowMessagesFromNonContacts, + showProfilePhotoToNonContacts: showProfilePhotoToNonContacts, + lastSeenVisibility: lastSeenVisibility, + showBioToNonContacts: showBioToNonContacts, + showStoriesToNonContacts: showStoriesToNonContacts, + allowServerChats: allowServerChats, + publicInvitePermission: publicInvitePermission, + groupInvitePermission: groupInvitePermission, + callPermission: callPermission, + forceAutoDeleteMessagesInPrivate: forceAutoDeleteMessagesInPrivate, + maxMessageAutoDeleteSeconds: maxMessageAutoDeleteSeconds, + autoDeleteAfterDays: autoDeleteAfterDays + ) + } +} + private extension EditPrivacyView { + func saveProfile() async { + let shouldProceed = await MainActor.run { () -> Bool in + if isSaving { + return false + } + isSaving = true + return true + } + + guard shouldProceed else { return } + + do { + let requestPayload = ProfileUpdateRequestPayload(profilePermissions: profilePermissions.requestPayload) + let responseMessage = try await profileService.updateProfile(requestPayload) + let fallback = NSLocalizedString("Настройки приватности обновлены.", comment: "Profile update success fallback") + let message = responseMessage.isEmpty ? fallback : responseMessage + + await MainActor.run { + alertData = AlertData(kind: .success, message: message) + isSaving = false + } + } catch { + let message: String + if let error = error as? LocalizedError, let description = error.errorDescription { + message = description + } else { + message = error.localizedDescription + } + + await MainActor.run { + alertData = AlertData(kind: .error, message: message) + isSaving = false + } + } + } + func loadProfile() async { await MainActor.run { if !isLoading { @@ -242,3 +321,14 @@ private extension EditPrivacyView { } } } + +private struct AlertData: Identifiable { + enum Kind { + case success + case error + } + + let id = UUID() + let kind: Kind + let message: String +}