ios_app_v2/yobble/Views/Tab/Settings/EditPrivacyView.swift
2025-10-08 03:16:34 +03:00

349 lines
14 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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("Профиль и поиск")) {
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
}