Compare commits
	
		
			3 Commits
		
	
	
		
			0dd8241958
			...
			fb8413e68c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fb8413e68c | |||
| c96fe4991d | |||
| 3b860a5146 | 
							
								
								
									
										117
									
								
								yobble/Network/ProfileModels.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								yobble/Network/ProfileModels.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,117 @@
 | 
				
			|||||||
 | 
					import Foundation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct ProfileDataPayload: Decodable {
 | 
				
			||||||
 | 
					    let userId: UUID
 | 
				
			||||||
 | 
					    let login: String
 | 
				
			||||||
 | 
					    let fullName: String?
 | 
				
			||||||
 | 
					    let bio: String?
 | 
				
			||||||
 | 
					    let balances: [WalletBalancePayload]
 | 
				
			||||||
 | 
					    let createdAt: Date?
 | 
				
			||||||
 | 
					    let stories: [JSONValue]
 | 
				
			||||||
 | 
					    let profilePermissions: ProfilePermissionsPayload
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private enum CodingKeys: String, CodingKey {
 | 
				
			||||||
 | 
					        case userId
 | 
				
			||||||
 | 
					        case login
 | 
				
			||||||
 | 
					        case fullName
 | 
				
			||||||
 | 
					        case bio
 | 
				
			||||||
 | 
					        case balances
 | 
				
			||||||
 | 
					        case createdAt
 | 
				
			||||||
 | 
					        case stories
 | 
				
			||||||
 | 
					        case profilePermissions
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    init(from decoder: Decoder) throws {
 | 
				
			||||||
 | 
					        let container = try decoder.container(keyedBy: CodingKeys.self)
 | 
				
			||||||
 | 
					        self.userId = try container.decode(UUID.self, forKey: .userId)
 | 
				
			||||||
 | 
					        self.login = try container.decode(String.self, forKey: .login)
 | 
				
			||||||
 | 
					        self.fullName = try container.decodeIfPresent(String.self, forKey: .fullName)
 | 
				
			||||||
 | 
					        self.bio = try container.decodeIfPresent(String.self, forKey: .bio)
 | 
				
			||||||
 | 
					        self.balances = try container.decodeIfPresent([WalletBalancePayload].self, forKey: .balances) ?? []
 | 
				
			||||||
 | 
					        self.createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt)
 | 
				
			||||||
 | 
					        self.stories = try container.decodeIfPresent([JSONValue].self, forKey: .stories) ?? []
 | 
				
			||||||
 | 
					        self.profilePermissions = try container.decode(ProfilePermissionsPayload.self, forKey: .profilePermissions)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct WalletBalancePayload: Decodable {
 | 
				
			||||||
 | 
					    let currency: String
 | 
				
			||||||
 | 
					    let balance: Decimal
 | 
				
			||||||
 | 
					    let displayBalance: Double?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private enum CodingKeys: String, CodingKey {
 | 
				
			||||||
 | 
					        case currency
 | 
				
			||||||
 | 
					        case balance
 | 
				
			||||||
 | 
					        case displayBalance
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    init(from decoder: Decoder) throws {
 | 
				
			||||||
 | 
					        let container = try decoder.container(keyedBy: CodingKeys.self)
 | 
				
			||||||
 | 
					        self.currency = try container.decode(String.self, forKey: .currency)
 | 
				
			||||||
 | 
					        self.balance = try Self.decodeDecimal(from: container, forKey: .balance)
 | 
				
			||||||
 | 
					        if let doubleValue = try? container.decode(Double.self, forKey: .displayBalance) {
 | 
				
			||||||
 | 
					            self.displayBalance = doubleValue
 | 
				
			||||||
 | 
					        } else if let stringValue = try? container.decode(String.self, forKey: .displayBalance),
 | 
				
			||||||
 | 
					                  let doubleValue = Double(stringValue) {
 | 
				
			||||||
 | 
					            self.displayBalance = doubleValue
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            self.displayBalance = nil
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static func decodeDecimal(from container: KeyedDecodingContainer<CodingKeys>, forKey key: CodingKeys) throws -> Decimal {
 | 
				
			||||||
 | 
					        if let decimalValue = try? container.decode(Decimal.self, forKey: key) {
 | 
				
			||||||
 | 
					            return decimalValue
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let stringValue = try? container.decode(String.self, forKey: key),
 | 
				
			||||||
 | 
					           let decimal = Decimal(string: stringValue) {
 | 
				
			||||||
 | 
					            return decimal
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let doubleValue = try? container.decode(Double.self, forKey: key) {
 | 
				
			||||||
 | 
					            return Decimal(doubleValue)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        throw DecodingError.dataCorruptedError(
 | 
				
			||||||
 | 
					            forKey: key,
 | 
				
			||||||
 | 
					            in: container,
 | 
				
			||||||
 | 
					            debugDescription: "Unable to decode decimal value for key \(key)"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct ProfilePermissionsPayload: Decodable {
 | 
				
			||||||
 | 
					    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 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										227
									
								
								yobble/Network/ProfileService.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										227
									
								
								yobble/Network/ProfileService.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,227 @@
 | 
				
			|||||||
 | 
					import Foundation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum ProfileServiceError: LocalizedError {
 | 
				
			||||||
 | 
					    case unexpectedStatus(String)
 | 
				
			||||||
 | 
					    case decoding(debugDescription: String)
 | 
				
			||||||
 | 
					    case encoding(String)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var errorDescription: String? {
 | 
				
			||||||
 | 
					        switch self {
 | 
				
			||||||
 | 
					        case .unexpectedStatus(let message):
 | 
				
			||||||
 | 
					            return message
 | 
				
			||||||
 | 
					        case .decoding(let debugDescription):
 | 
				
			||||||
 | 
					            return AppConfig.DEBUG
 | 
				
			||||||
 | 
					                ? debugDescription
 | 
				
			||||||
 | 
					                : NSLocalizedString("Не удалось загрузить профиль.", comment: "Profile service decoding error")
 | 
				
			||||||
 | 
					        case .encoding(let message):
 | 
				
			||||||
 | 
					            return message
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final class ProfileService {
 | 
				
			||||||
 | 
					    private let client: NetworkClient
 | 
				
			||||||
 | 
					    private let decoder: JSONDecoder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    init(client: NetworkClient = .shared) {
 | 
				
			||||||
 | 
					        self.client = client
 | 
				
			||||||
 | 
					        self.decoder = JSONDecoder()
 | 
				
			||||||
 | 
					        self.decoder.keyDecodingStrategy = .convertFromSnakeCase
 | 
				
			||||||
 | 
					        self.decoder.dateDecodingStrategy = .custom(Self.decodeDate)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    func fetchMyProfile(completion: @escaping (Result<ProfileDataPayload, Error>) -> Void) {
 | 
				
			||||||
 | 
					        client.request(
 | 
				
			||||||
 | 
					            path: "/v1/profile/me",
 | 
				
			||||||
 | 
					            method: .get,
 | 
				
			||||||
 | 
					            requiresAuth: true
 | 
				
			||||||
 | 
					        ) { [decoder] result in
 | 
				
			||||||
 | 
					            switch result {
 | 
				
			||||||
 | 
					            case .success(let response):
 | 
				
			||||||
 | 
					                do {
 | 
				
			||||||
 | 
					                    let apiResponse = try decoder.decode(APIResponse<ProfileDataPayload>.self, from: response.data)
 | 
				
			||||||
 | 
					                    guard apiResponse.status == "fine" else {
 | 
				
			||||||
 | 
					                        let message = apiResponse.detail ?? NSLocalizedString("Не удалось загрузить профиль.", comment: "Profile unexpected status")
 | 
				
			||||||
 | 
					                        completion(.failure(ProfileServiceError.unexpectedStatus(message)))
 | 
				
			||||||
 | 
					                        return
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    completion(.success(apiResponse.data))
 | 
				
			||||||
 | 
					                } catch {
 | 
				
			||||||
 | 
					                    let debugMessage = Self.describeDecodingError(error: error, data: response.data)
 | 
				
			||||||
 | 
					                    if AppConfig.DEBUG {
 | 
				
			||||||
 | 
					                        print("[ProfileService] decode profile failed: \(debugMessage)")
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    completion(.failure(ProfileServiceError.decoding(debugDescription: debugMessage)))
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            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 fetchMyProfile() async throws -> ProfileDataPayload {
 | 
				
			||||||
 | 
					        try await withCheckedThrowingContinuation { continuation in
 | 
				
			||||||
 | 
					            fetchMyProfile { result in
 | 
				
			||||||
 | 
					                continuation.resume(with: result)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    func updateProfile(_ payload: ProfileUpdateRequestPayload, completion: @escaping (Result<String, Error>) -> 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<MessagePayload>.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)
 | 
				
			||||||
 | 
					        if let date = iso8601WithFractionalSeconds.date(from: string) {
 | 
				
			||||||
 | 
					            return date
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let date = iso8601Simple.date(from: string) {
 | 
				
			||||||
 | 
					            return date
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        throw DecodingError.dataCorruptedError(
 | 
				
			||||||
 | 
					            in: container,
 | 
				
			||||||
 | 
					            debugDescription: "Невозможно декодировать дату: \(string)"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static func describeDecodingError(error: Error, data: Data) -> String {
 | 
				
			||||||
 | 
					        var parts: [String] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let decodingError = error as? DecodingError {
 | 
				
			||||||
 | 
					            parts.append(decodingDescription(from: decodingError))
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            parts.append(error.localizedDescription)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let payload = truncatedPayload(from: data) {
 | 
				
			||||||
 | 
					            parts.append("payload=\(payload)")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return parts.joined(separator: "\n")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static func decodingDescription(from error: DecodingError) -> String {
 | 
				
			||||||
 | 
					        switch error {
 | 
				
			||||||
 | 
					        case .typeMismatch(let type, let context):
 | 
				
			||||||
 | 
					            return "Type mismatch for \(type) at \(codingPath(from: context)): \(context.debugDescription)"
 | 
				
			||||||
 | 
					        case .valueNotFound(let type, let context):
 | 
				
			||||||
 | 
					            return "Value not found for \(type) at \(codingPath(from: context)): \(context.debugDescription)"
 | 
				
			||||||
 | 
					        case .keyNotFound(let key, let context):
 | 
				
			||||||
 | 
					            return "Missing key '\(key.stringValue)' at \(codingPath(from: context)): \(context.debugDescription)"
 | 
				
			||||||
 | 
					        case .dataCorrupted(let context):
 | 
				
			||||||
 | 
					            return "Corrupted data at \(codingPath(from: context)): \(context.debugDescription)"
 | 
				
			||||||
 | 
					        @unknown default:
 | 
				
			||||||
 | 
					            return error.localizedDescription
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static func codingPath(from context: DecodingError.Context) -> String {
 | 
				
			||||||
 | 
					        let path = context.codingPath.map { $0.stringValue }.filter { !$0.isEmpty }
 | 
				
			||||||
 | 
					        return path.isEmpty ? "root" : path.joined(separator: ".")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static func truncatedPayload(from data: Data, limit: Int = 512) -> String? {
 | 
				
			||||||
 | 
					        guard let string = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),
 | 
				
			||||||
 | 
					              !string.isEmpty else {
 | 
				
			||||||
 | 
					            return nil
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if string.count <= limit {
 | 
				
			||||||
 | 
					            return string
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let index = string.index(string.startIndex, offsetBy: limit)
 | 
				
			||||||
 | 
					        return String(string[string.startIndex..<index]) + "…"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static func errorMessage(from data: Data) -> String? {
 | 
				
			||||||
 | 
					        if let apiError = try? JSONDecoder().decode(ErrorResponse.self, from: data) {
 | 
				
			||||||
 | 
					            if let detail = apiError.detail, !detail.isEmpty {
 | 
				
			||||||
 | 
					                return detail
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if let message = apiError.data?.message, !message.isEmpty {
 | 
				
			||||||
 | 
					                return message
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let string = String(data: data, encoding: .utf8), !string.isEmpty {
 | 
				
			||||||
 | 
					            return string
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return nil
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static let iso8601WithFractionalSeconds: ISO8601DateFormatter = {
 | 
				
			||||||
 | 
					        let formatter = ISO8601DateFormatter()
 | 
				
			||||||
 | 
					        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
 | 
				
			||||||
 | 
					        return formatter
 | 
				
			||||||
 | 
					    }()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static let iso8601Simple: ISO8601DateFormatter = {
 | 
				
			||||||
 | 
					        let formatter = ISO8601DateFormatter()
 | 
				
			||||||
 | 
					        formatter.formatOptions = [.withInternetDateTime]
 | 
				
			||||||
 | 
					        return formatter
 | 
				
			||||||
 | 
					    }()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -74,6 +74,7 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "OK" : {
 | 
					    "OK" : {
 | 
				
			||||||
 | 
					      "comment" : "Profile update alert button",
 | 
				
			||||||
      "localizations" : {
 | 
					      "localizations" : {
 | 
				
			||||||
        "en" : {
 | 
					        "en" : {
 | 
				
			||||||
          "stringUnit" : {
 | 
					          "stringUnit" : {
 | 
				
			||||||
@ -219,6 +220,9 @@
 | 
				
			|||||||
    "Глобальный поиск" : {
 | 
					    "Глобальный поиск" : {
 | 
				
			||||||
      "comment" : "Global search section"
 | 
					      "comment" : "Global search section"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "Готово" : {
 | 
				
			||||||
 | 
					      "comment" : "Profile update success title"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "Данные" : {
 | 
					    "Данные" : {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -570,10 +574,16 @@
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    "Настройки приватности" : {
 | 
					    "Настройки приватности" : {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "Настройки приватности обновлены." : {
 | 
				
			||||||
 | 
					      "comment" : "Profile update success fallback"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "Не удалось выполнить поиск." : {
 | 
					    "Не удалось выполнить поиск." : {
 | 
				
			||||||
      "comment" : "Search error fallback\nSearch service decoding error"
 | 
					      "comment" : "Search error fallback\nSearch service decoding error"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "Не удалось загрузить профиль." : {
 | 
				
			||||||
 | 
					      "comment" : "Profile service decoding error\nProfile unexpected status"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "Не удалось загрузить список чатов." : {
 | 
					    "Не удалось загрузить список чатов." : {
 | 
				
			||||||
      "localizations" : {
 | 
					      "localizations" : {
 | 
				
			||||||
        "en" : {
 | 
					        "en" : {
 | 
				
			||||||
@ -618,6 +628,7 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "Не удалось обработать ответ сервера." : {
 | 
					    "Не удалось обработать ответ сервера." : {
 | 
				
			||||||
 | 
					      "comment" : "Profile update decode error",
 | 
				
			||||||
      "localizations" : {
 | 
					      "localizations" : {
 | 
				
			||||||
        "en" : {
 | 
					        "en" : {
 | 
				
			||||||
          "stringUnit" : {
 | 
					          "stringUnit" : {
 | 
				
			||||||
@ -627,6 +638,9 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "Не удалось подготовить данные запроса." : {
 | 
				
			||||||
 | 
					      "comment" : "Profile update encoding error"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "Не удалось сериализовать данные запроса." : {
 | 
					    "Не удалось сериализовать данные запроса." : {
 | 
				
			||||||
      "localizations" : {
 | 
					      "localizations" : {
 | 
				
			||||||
        "en" : {
 | 
					        "en" : {
 | 
				
			||||||
@ -637,6 +651,9 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "Не удалось сохранить изменения профиля." : {
 | 
				
			||||||
 | 
					      "comment" : "Profile update unexpected status"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "Неверный запрос (400)." : {
 | 
					    "Неверный запрос (400)." : {
 | 
				
			||||||
      "localizations" : {
 | 
					      "localizations" : {
 | 
				
			||||||
        "en" : {
 | 
					        "en" : {
 | 
				
			||||||
@ -820,6 +837,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "Ошибка" : {
 | 
					    "Ошибка" : {
 | 
				
			||||||
 | 
					      "comment" : "Profile update error title",
 | 
				
			||||||
      "localizations" : {
 | 
					      "localizations" : {
 | 
				
			||||||
        "en" : {
 | 
					        "en" : {
 | 
				
			||||||
          "stringUnit" : {
 | 
					          "stringUnit" : {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,13 @@
 | 
				
			|||||||
import SwiftUI
 | 
					import SwiftUI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct EditPrivacyView: View {
 | 
					struct EditPrivacyView: View {
 | 
				
			||||||
    @State private var profilePermissions = ProfilePermissionsResponse()
 | 
					    @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()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private var privacyScopeOptions: [PrivacyScope] { PrivacyScope.allCases }
 | 
					    private var privacyScopeOptions: [PrivacyScope] { PrivacyScope.allCases }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -30,85 +36,121 @@ struct EditPrivacyView: View {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    var body: some View {
 | 
					    var body: some View {
 | 
				
			||||||
        Form {
 | 
					        Form {
 | 
				
			||||||
            Section(header: Text("Профиль и поиск")) {
 | 
					            if isLoading {
 | 
				
			||||||
                Toggle("Разрешить поиск профиля", isOn: $profilePermissions.isSearchable)
 | 
					                Section {
 | 
				
			||||||
                Toggle("Разрешить пересылку сообщений", isOn: $profilePermissions.allowMessageForwarding)
 | 
					                    ProgressView()
 | 
				
			||||||
                Toggle("Принимать сообщения от незнакомцев", isOn: $profilePermissions.allowMessagesFromNonContacts)
 | 
					                        .frame(maxWidth: .infinity, alignment: .center)
 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Section(header: Text("Видимость и контент")) {
 | 
					 | 
				
			||||||
                Toggle("Показывать фото не-контактам", isOn: $profilePermissions.showProfilePhotoToNonContacts)
 | 
					 | 
				
			||||||
                Toggle("Показывать био не-контактам", isOn: $profilePermissions.showBioToNonContacts)
 | 
					 | 
				
			||||||
                Toggle("Показывать сторисы не-контактам", isOn: $profilePermissions.showStoriesToNonContacts)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                Picker("Видимость статуса 'был в сети'", selection: $profilePermissions.lastSeenVisibility) {
 | 
					 | 
				
			||||||
                    ForEach(privacyScopeOptions) { scope in
 | 
					 | 
				
			||||||
                        Text(scope.title).tag(scope.rawValue)
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                .pickerStyle(.segmented)
 | 
					            } else if let loadError {
 | 
				
			||||||
            }
 | 
					                Section {
 | 
				
			||||||
 | 
					                    Text(loadError)
 | 
				
			||||||
            Section(header: Text("Приглашения и звонки")) {
 | 
					                        .foregroundColor(.red)
 | 
				
			||||||
                Picker("Кто может приглашать в паблики", selection: $profilePermissions.publicInvitePermission) {
 | 
					 | 
				
			||||||
                    ForEach(privacyScopeOptions) { scope in
 | 
					 | 
				
			||||||
                        Text(scope.title).tag(scope.rawValue)
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                .pickerStyle(.segmented)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                Picker("Кто может приглашать в беседы", selection: $profilePermissions.groupInvitePermission) {
 | 
					 | 
				
			||||||
                    ForEach(privacyScopeOptions) { scope in
 | 
					 | 
				
			||||||
                        Text(scope.title).tag(scope.rawValue)
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                .pickerStyle(.segmented)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                Picker("Кто может звонить", selection: $profilePermissions.callPermission) {
 | 
					 | 
				
			||||||
                    ForEach(privacyScopeOptions) { scope in
 | 
					 | 
				
			||||||
                        Text(scope.title).tag(scope.rawValue)
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                .pickerStyle(.segmented)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Section(header: Text("Чаты и хранение")) {
 | 
					 | 
				
			||||||
                Toggle("Разрешить хранить чаты на сервере (Обычный)", isOn: $profilePermissions.allowServerChats)
 | 
					 | 
				
			||||||
                Toggle("Принудительное автоудаление в ЛС (Приватный)", isOn: $profilePermissions.forceAutoDeleteMessagesInPrivate)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if profilePermissions.forceAutoDeleteMessagesInPrivate {
 | 
					 | 
				
			||||||
                    Stepper(value: forceAutoDeleteBinding, in: 5...86400, step: 5) {
 | 
					 | 
				
			||||||
                        Text("Таймер автоудаления: \(formattedAutoDeleteSeconds(forceAutoDeleteBinding.wrappedValue))")
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Section(header: Text("Автоудаление аккаунта")) {
 | 
					 | 
				
			||||||
                Toggle("Включить автоудаление аккаунта", isOn: autoDeleteAccountEnabled)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if autoDeleteAccountEnabled.wrappedValue {
 | 
					 | 
				
			||||||
                    Stepper(value: autoDeleteAccountBinding, in: 1...365) {
 | 
					 | 
				
			||||||
                        Text("Удалять аккаунт через \(autoDeleteAccountBinding.wrappedValue) дн.")
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Section {
 | 
					 | 
				
			||||||
                Button("Сохранить изменения") {
 | 
					 | 
				
			||||||
                    print("Параметры приватности: \(profilePermissions)")
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                .frame(maxWidth: .infinity, alignment: .center)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            Section {
 | 
					 | 
				
			||||||
                Button(role: .destructive) {
 | 
					 | 
				
			||||||
                    profilePermissions = ProfilePermissionsResponse()
 | 
					 | 
				
			||||||
                    print("Настройки приватности сброшены к значениям по умолчанию")
 | 
					 | 
				
			||||||
                } label: {
 | 
					 | 
				
			||||||
                    Text("Сбросить по умолчанию")
 | 
					 | 
				
			||||||
                        .frame(maxWidth: .infinity, alignment: .center)
 | 
					                        .frame(maxWidth: .infinity, alignment: .center)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if !isLoading && loadError == nil {
 | 
				
			||||||
 | 
					                Section(header: Text("Профиль и поиск")) {
 | 
				
			||||||
 | 
					                    Toggle("Разрешить поиск профиля", isOn: $profilePermissions.isSearchable)
 | 
				
			||||||
 | 
					                    Toggle("Разрешить пересылку сообщений", isOn: $profilePermissions.allowMessageForwarding)
 | 
				
			||||||
 | 
					                    Toggle("Принимать сообщения от незнакомцев", isOn: $profilePermissions.allowMessagesFromNonContacts)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                Section(header: Text("Видимость и контент")) {
 | 
				
			||||||
 | 
					                    Toggle("Показывать фото не-контактам", isOn: $profilePermissions.showProfilePhotoToNonContacts)
 | 
				
			||||||
 | 
					                    Toggle("Показывать био не-контактам", isOn: $profilePermissions.showBioToNonContacts)
 | 
				
			||||||
 | 
					                    Toggle("Показывать сторисы не-контактам", isOn: $profilePermissions.showStoriesToNonContacts)
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    Picker("Видимость статуса 'был в сети'", selection: $profilePermissions.lastSeenVisibility) {
 | 
				
			||||||
 | 
					                        ForEach(privacyScopeOptions) { scope in
 | 
				
			||||||
 | 
					                            Text(scope.title).tag(scope.rawValue)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    .pickerStyle(.segmented)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                Section(header: Text("Приглашения и звонки")) {
 | 
				
			||||||
 | 
					                    Picker("Кто может приглашать в паблики", selection: $profilePermissions.publicInvitePermission) {
 | 
				
			||||||
 | 
					                        ForEach(privacyScopeOptions) { scope in
 | 
				
			||||||
 | 
					                            Text(scope.title).tag(scope.rawValue)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    .pickerStyle(.segmented)
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    Picker("Кто может приглашать в беседы", selection: $profilePermissions.groupInvitePermission) {
 | 
				
			||||||
 | 
					                        ForEach(privacyScopeOptions) { scope in
 | 
				
			||||||
 | 
					                            Text(scope.title).tag(scope.rawValue)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    .pickerStyle(.segmented)
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    Picker("Кто может звонить", selection: $profilePermissions.callPermission) {
 | 
				
			||||||
 | 
					                        ForEach(privacyScopeOptions) { scope in
 | 
				
			||||||
 | 
					                            Text(scope.title).tag(scope.rawValue)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    .pickerStyle(.segmented)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                Section(header: Text("Чаты и хранение")) {
 | 
				
			||||||
 | 
					                    Toggle("Разрешить хранить чаты на сервере (Обычный)", isOn: $profilePermissions.allowServerChats)
 | 
				
			||||||
 | 
					                    Toggle("Принудительное автоудаление в ЛС (Приватный)", isOn: $profilePermissions.forceAutoDeleteMessagesInPrivate)
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    if profilePermissions.forceAutoDeleteMessagesInPrivate {
 | 
				
			||||||
 | 
					                        Stepper(value: forceAutoDeleteBinding, in: 5...86400, step: 5) {
 | 
				
			||||||
 | 
					                            Text("Таймер автоудаления: \(formattedAutoDeleteSeconds(forceAutoDeleteBinding.wrappedValue))")
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                Section(header: Text("Автоудаление аккаунта")) {
 | 
				
			||||||
 | 
					                    Toggle("Включить автоудаление аккаунта", isOn: autoDeleteAccountEnabled)
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    if autoDeleteAccountEnabled.wrappedValue {
 | 
				
			||||||
 | 
					                        Stepper(value: autoDeleteAccountBinding, in: 1...365) {
 | 
				
			||||||
 | 
					                            Text("Удалять аккаунт через \(autoDeleteAccountBinding.wrappedValue) дн.")
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                Section {
 | 
				
			||||||
 | 
					                    Button {
 | 
				
			||||||
 | 
					                        Task {
 | 
				
			||||||
 | 
					                            await saveProfile()
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } label: {
 | 
				
			||||||
 | 
					                        if isSaving {
 | 
				
			||||||
 | 
					                            ProgressView()
 | 
				
			||||||
 | 
					                                .frame(maxWidth: .infinity, alignment: .center)
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            Text("Сохранить изменения")
 | 
				
			||||||
 | 
					                                .frame(maxWidth: .infinity, alignment: .center)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    .disabled(isLoading || isSaving)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                Section {
 | 
				
			||||||
 | 
					                    Button(role: .destructive) {
 | 
				
			||||||
 | 
					                        profilePermissions = ProfilePermissionsState()
 | 
				
			||||||
 | 
					                        print("Настройки приватности сброшены к значениям по умолчанию")
 | 
				
			||||||
 | 
					                    } label: {
 | 
				
			||||||
 | 
					                        Text("Сбросить по умолчанию")
 | 
				
			||||||
 | 
					                            .frame(maxWidth: .infinity, alignment: .center)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .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("Настройки приватности")
 | 
					        .navigationTitle("Настройки приватности")
 | 
				
			||||||
        .onChange(of: profilePermissions.forceAutoDeleteMessagesInPrivate) { newValue in
 | 
					        .onChange(of: profilePermissions.forceAutoDeleteMessagesInPrivate) { newValue in
 | 
				
			||||||
@ -118,6 +160,9 @@ struct EditPrivacyView: View {
 | 
				
			|||||||
                profilePermissions.maxMessageAutoDeleteSeconds = nil
 | 
					                profilePermissions.maxMessageAutoDeleteSeconds = nil
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        .task {
 | 
				
			||||||
 | 
					            await loadProfile()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private func formattedAutoDeleteSeconds(_ value: Int) -> String {
 | 
					    private func formattedAutoDeleteSeconds(_ value: Int) -> String {
 | 
				
			||||||
@ -153,7 +198,7 @@ private enum PrivacyScope: Int, CaseIterable, Identifiable {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct ProfilePermissionsResponse: Codable {
 | 
					struct ProfilePermissionsState: Codable, Equatable {
 | 
				
			||||||
    var isSearchable: Bool = true
 | 
					    var isSearchable: Bool = true
 | 
				
			||||||
    var allowMessageForwarding: Bool = true
 | 
					    var allowMessageForwarding: Bool = true
 | 
				
			||||||
    var allowMessagesFromNonContacts: Bool = true
 | 
					    var allowMessagesFromNonContacts: Bool = true
 | 
				
			||||||
@ -169,3 +214,121 @@ struct ProfilePermissionsResponse: Codable {
 | 
				
			|||||||
    var maxMessageAutoDeleteSeconds: Int? = nil
 | 
					    var maxMessageAutoDeleteSeconds: Int? = nil
 | 
				
			||||||
    var autoDeleteAfterDays: Int? = nil
 | 
					    var autoDeleteAfterDays: Int? = nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					extension ProfilePermissionsState {
 | 
				
			||||||
 | 
					    init(payload: ProfilePermissionsPayload) {
 | 
				
			||||||
 | 
					        self.isSearchable = payload.isSearchable
 | 
				
			||||||
 | 
					        self.allowMessageForwarding = payload.allowMessageForwarding
 | 
				
			||||||
 | 
					        self.allowMessagesFromNonContacts = payload.allowMessagesFromNonContacts
 | 
				
			||||||
 | 
					        self.showProfilePhotoToNonContacts = payload.showProfilePhotoToNonContacts
 | 
				
			||||||
 | 
					        self.lastSeenVisibility = payload.lastSeenVisibility
 | 
				
			||||||
 | 
					        self.showBioToNonContacts = payload.showBioToNonContacts
 | 
				
			||||||
 | 
					        self.showStoriesToNonContacts = payload.showStoriesToNonContacts
 | 
				
			||||||
 | 
					        self.allowServerChats = payload.allowServerChats
 | 
				
			||||||
 | 
					        self.publicInvitePermission = payload.publicInvitePermission
 | 
				
			||||||
 | 
					        self.groupInvitePermission = payload.groupInvitePermission
 | 
				
			||||||
 | 
					        self.callPermission = payload.callPermission
 | 
				
			||||||
 | 
					        self.forceAutoDeleteMessagesInPrivate = payload.forceAutoDeleteMessagesInPrivate
 | 
				
			||||||
 | 
					        self.maxMessageAutoDeleteSeconds = payload.maxMessageAutoDeleteSeconds
 | 
				
			||||||
 | 
					        self.autoDeleteAfterDays = payload.autoDeleteAfterDays
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 {
 | 
				
			||||||
 | 
					                isLoading = true
 | 
				
			||||||
 | 
					                loadError = nil
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        do {
 | 
				
			||||||
 | 
					            let profile = try await profileService.fetchMyProfile()
 | 
				
			||||||
 | 
					            await MainActor.run {
 | 
				
			||||||
 | 
					                profilePermissions = ProfilePermissionsState(payload: profile.profilePermissions)
 | 
				
			||||||
 | 
					                isLoading = false
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch {
 | 
				
			||||||
 | 
					            let message: String
 | 
				
			||||||
 | 
					            if let error = error as? LocalizedError, let description = error.errorDescription {
 | 
				
			||||||
 | 
					                message = description
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                message = error.localizedDescription
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await MainActor.run {
 | 
				
			||||||
 | 
					                loadError = message
 | 
				
			||||||
 | 
					                isLoading = false
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private struct AlertData: Identifiable {
 | 
				
			||||||
 | 
					    enum Kind {
 | 
				
			||||||
 | 
					        case success
 | 
				
			||||||
 | 
					        case error
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let id = UUID()
 | 
				
			||||||
 | 
					    let kind: Kind
 | 
				
			||||||
 | 
					    let message: String
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user