350 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			350 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
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<Bool> {
 | 
						||
        Binding(
 | 
						||
            get: { profilePermissions.autoDeleteAfterDays != nil },
 | 
						||
            set: { newValue in
 | 
						||
                profilePermissions.autoDeleteAfterDays = newValue ? (profilePermissions.autoDeleteAfterDays ?? 30) : nil
 | 
						||
            }
 | 
						||
        )
 | 
						||
    }
 | 
						||
 | 
						||
    private var autoDeleteAccountBinding: Binding<Int> {
 | 
						||
        Binding(
 | 
						||
            get: { profilePermissions.autoDeleteAfterDays ?? 30 },
 | 
						||
            set: { profilePermissions.autoDeleteAfterDays = min(max($0, 1), 365) }
 | 
						||
        )
 | 
						||
    }
 | 
						||
 | 
						||
    private var autoDeleteTimerEnabled: Binding<Bool> {
 | 
						||
        Binding(
 | 
						||
            get: { profilePermissions.maxMessageAutoDeleteSeconds != nil },
 | 
						||
            set: { newValue in
 | 
						||
                profilePermissions.maxMessageAutoDeleteSeconds = newValue ? (profilePermissions.maxMessageAutoDeleteSeconds ?? 30) : nil
 | 
						||
            }
 | 
						||
        )
 | 
						||
    }
 | 
						||
 | 
						||
    private var autoDeleteTimerBinding: Binding<Int> {
 | 
						||
        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(NSLocalizedString("Профиль и поиск", comment: ""))) {
 | 
						||
                    Toggle(NSLocalizedString("Разрешить поиск профиля", comment: ""), isOn: $profilePermissions.isSearchable)
 | 
						||
                    Toggle(NSLocalizedString("Разрешить пересылку сообщений", comment: ""), isOn: $profilePermissions.allowMessageForwarding)
 | 
						||
                    Toggle(NSLocalizedString("Принимать сообщения от незнакомцев", comment: ""), isOn: $profilePermissions.allowMessagesFromNonContacts)
 | 
						||
                }
 | 
						||
                
 | 
						||
                Section(header: Text(NSLocalizedString("Видимость и контент", comment: ""))) {
 | 
						||
                    Toggle(NSLocalizedString("Показывать фото не-контактам", comment: ""), isOn: $profilePermissions.showProfilePhotoToNonContacts)
 | 
						||
                    Toggle(NSLocalizedString("Показывать био не-контактам", comment: ""), isOn: $profilePermissions.showBioToNonContacts)
 | 
						||
                    Toggle(NSLocalizedString("Показывать сторисы не-контактам", comment: ""), isOn: $profilePermissions.showStoriesToNonContacts)
 | 
						||
                    
 | 
						||
                    Picker(NSLocalizedString("Видимость статуса 'был в сети'", comment: ""), selection: $profilePermissions.lastSeenVisibility) {
 | 
						||
                        ForEach(privacyScopeOptions) { scope in
 | 
						||
                            Text(scope.title).tag(scope.rawValue)
 | 
						||
                        }
 | 
						||
                    }
 | 
						||
                    .pickerStyle(.segmented)
 | 
						||
                }
 | 
						||
                
 | 
						||
                Section(header: Text(NSLocalizedString("Приглашения и звонки", comment: ""))) {
 | 
						||
                    Picker(NSLocalizedString("Кто может приглашать в паблики", comment: ""), selection: $profilePermissions.publicInvitePermission) {
 | 
						||
                        ForEach(privacyScopeOptions) { scope in
 | 
						||
                            Text(scope.title).tag(scope.rawValue)
 | 
						||
                        }
 | 
						||
                    }
 | 
						||
                    .pickerStyle(.segmented)
 | 
						||
                    
 | 
						||
                    Picker(NSLocalizedString("Кто может приглашать в беседы", comment: ""), selection: $profilePermissions.groupInvitePermission) {
 | 
						||
                        ForEach(privacyScopeOptions) { scope in
 | 
						||
                            Text(scope.title).tag(scope.rawValue)
 | 
						||
                        }
 | 
						||
                    }
 | 
						||
                    .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: ""))) {
 | 
						||
                    Toggle(NSLocalizedString("Разрешить хранить чаты на сервере", comment: ""), isOn: $profilePermissions.allowServerChats)
 | 
						||
                }
 | 
						||
                
 | 
						||
                Section(header: Text(NSLocalizedString("Приватные чаты", comment: ""))) {
 | 
						||
                    Toggle(NSLocalizedString("Принудительное включение автоудаления сообщений", comment: ""), isOn: $profilePermissions.forceAutoDeleteMessagesInPrivate)
 | 
						||
                    Toggle(NSLocalizedString("Ограничить таймер автоудаления (максимум)", comment: ""), isOn: autoDeleteTimerEnabled)
 | 
						||
                    
 | 
						||
                    if autoDeleteTimerEnabled.wrappedValue {
 | 
						||
                        Stepper(value: autoDeleteTimerBinding, in: 5...86400, step: 5) {
 | 
						||
                            Text("\(NSLocalizedString("Максимальное время автоудаления", comment: "")): \(formattedAutoDeleteSeconds(autoDeleteTimerBinding.wrappedValue))")
 | 
						||
                        }
 | 
						||
                    }
 | 
						||
                }
 | 
						||
                
 | 
						||
                Section(header: Text(NSLocalizedString("Автоудаление аккаунта", comment: ""))) {
 | 
						||
                    Toggle(NSLocalizedString("Включить автоудаление аккаунта", comment: ""), isOn: autoDeleteAccountEnabled)
 | 
						||
                    
 | 
						||
                    if autoDeleteAccountEnabled.wrappedValue {
 | 
						||
                        Stepper(value: autoDeleteAccountBinding, in: 1...365) {
 | 
						||
                            Text(String(format: NSLocalizedString("Удалять аккаунт через %lld дн.", comment: ""), autoDeleteAccountBinding.wrappedValue))
 | 
						||
 | 
						||
                        }
 | 
						||
                    }
 | 
						||
                }
 | 
						||
                
 | 
						||
                Section {
 | 
						||
                    Button {
 | 
						||
                        Task {
 | 
						||
                            await saveProfile()
 | 
						||
                        }
 | 
						||
                    } label: {
 | 
						||
                        if isSaving {
 | 
						||
                            ProgressView()
 | 
						||
                                .frame(maxWidth: .infinity, alignment: .center)
 | 
						||
                        } else {
 | 
						||
                            Text(NSLocalizedString("Сохранить изменения", comment: ""))
 | 
						||
                                .frame(maxWidth: .infinity, alignment: .center)
 | 
						||
                        }
 | 
						||
                    }
 | 
						||
                    .disabled(isLoading || isSaving)
 | 
						||
                }
 | 
						||
                
 | 
						||
                Section {
 | 
						||
                    Button(role: .destructive) {
 | 
						||
                        profilePermissions = ProfilePermissionsState()
 | 
						||
                        if AppConfig.DEBUG {print("Настройки приватности сброшены к значениям по умолчанию")}
 | 
						||
                    } label: {
 | 
						||
                        Text(NSLocalizedString("Сбросить по умолчанию", comment: ""))
 | 
						||
                            .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(NSLocalizedString("Настройки приватности", comment: ""))
 | 
						||
        .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 NSLocalizedString("Все", comment: "")
 | 
						||
        case .contacts:
 | 
						||
            return NSLocalizedString("Контакты", comment: "")
 | 
						||
        case .nobody:
 | 
						||
            return NSLocalizedString("Никто", comment: "")
 | 
						||
        }
 | 
						||
    }
 | 
						||
}
 | 
						||
 | 
						||
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
 | 
						||
}
 |