343 lines
14 KiB
Swift
343 lines
14 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 forceAutoDeleteBinding: Binding<Int> {
|
||
Binding(
|
||
get: { profilePermissions.maxMessageAutoDeleteSeconds ?? 30 },
|
||
set: { profilePermissions.maxMessageAutoDeleteSeconds = $0 }
|
||
)
|
||
}
|
||
|
||
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) }
|
||
)
|
||
}
|
||
|
||
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)
|
||
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("Настройки приватности")
|
||
.onChange(of: profilePermissions.forceAutoDeleteMessagesInPrivate) { newValue in
|
||
if newValue {
|
||
profilePermissions.maxMessageAutoDeleteSeconds = profilePermissions.maxMessageAutoDeleteSeconds ?? 30
|
||
} else {
|
||
profilePermissions.maxMessageAutoDeleteSeconds = nil
|
||
}
|
||
}
|
||
.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
|
||
}
|