import SwiftUI 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() private var privacyScopeOptions: [PrivacyScope] { PrivacyScope.allCases } private var autoDeleteAccountEnabled: Binding { Binding( get: { profilePermissions.autoDeleteAfterDays != nil }, set: { newValue in profilePermissions.autoDeleteAfterDays = newValue ? (profilePermissions.autoDeleteAfterDays ?? 30) : nil } ) } private var autoDeleteAccountBinding: Binding { Binding( get: { profilePermissions.autoDeleteAfterDays ?? 30 }, set: { profilePermissions.autoDeleteAfterDays = min(max($0, 1), 365) } ) } private var autoDeleteTimerEnabled: Binding { Binding( get: { profilePermissions.maxMessageAutoDeleteSeconds != nil }, set: { newValue in profilePermissions.maxMessageAutoDeleteSeconds = newValue ? (profilePermissions.maxMessageAutoDeleteSeconds ?? 30) : nil } ) } private var autoDeleteTimerBinding: Binding { Binding( get: { profilePermissions.maxMessageAutoDeleteSeconds ?? 30 }, set: { profilePermissions.maxMessageAutoDeleteSeconds = min(max($0, 5), 86400) } ) } var body: some View { Form { if isLoading { Section { ProgressView() .frame(maxWidth: .infinity, alignment: .center) } } else if let loadError { Section { Text(loadError) .foregroundColor(.red) .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) } Section(header: Text("Приватные чаты")) { Toggle("Принудительное включение автоудаление сообщений", isOn: $profilePermissions.forceAutoDeleteMessagesInPrivate) Toggle("Включить ограничитель таймера автоудаления в ЛС", isOn: autoDeleteTimerEnabled) if autoDeleteTimerEnabled.wrappedValue { Stepper(value: autoDeleteTimerBinding, in: 5...86400, step: 5) { Text("Ограничить таймер автоудаления (максимум): \(formattedAutoDeleteSeconds(autoDeleteTimerBinding.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("Настройки приватности") .task { await loadProfile() } } private func formattedAutoDeleteSeconds(_ value: Int) -> String { let secondsString = "\(value) сек." switch value { case ..<60: return secondsString case 60..<3600: let minutes = value / 60 return "\(secondsString) (≈ \(minutes) мин.)" default: let hours = Double(value) / 3600.0 let formattedHours: String if hours.truncatingRemainder(dividingBy: 1) == 0 { formattedHours = String(format: "%.0f", hours) } else { formattedHours = String(format: "%.1f", hours) } return "\(secondsString) (≈ \(formattedHours) ч.)" } } } private enum PrivacyScope: Int, CaseIterable, Identifiable { case everyone = 0 case contacts = 1 case nobody = 2 var id: Int { rawValue } var title: String { switch self { case .everyone: return "Все" case .contacts: return "Контакты" case .nobody: return "Никто" } } } struct ProfilePermissionsState: Codable, Equatable { var isSearchable: Bool = true var allowMessageForwarding: Bool = true var allowMessagesFromNonContacts: Bool = true var showProfilePhotoToNonContacts: Bool = true var lastSeenVisibility: Int = PrivacyScope.everyone.rawValue var showBioToNonContacts: Bool = true var showStoriesToNonContacts: Bool = true var allowServerChats: Bool = true var publicInvitePermission: Int = PrivacyScope.everyone.rawValue var groupInvitePermission: Int = PrivacyScope.everyone.rawValue var callPermission: Int = PrivacyScope.everyone.rawValue var forceAutoDeleteMessagesInPrivate: Bool = false var maxMessageAutoDeleteSeconds: 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 }