Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions Convos/App Settings/AppSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ struct ConvosToolbarButton: View {
// swiftlint:disable force_unwrapping

struct AppSettingsView: View {
@Bindable var viewModel: AppSettingsViewModel
@Bindable var quicknameViewModel: QuicknameSettingsViewModel
let onDeleteAllData: () -> Void
@State private var showingDeleteAllDataConfirmation: Bool = false
Expand Down Expand Up @@ -161,16 +162,15 @@ struct AppSettingsView: View {
} label: {
Text("Delete all app data")
}
.confirmationDialog("", isPresented: $showingDeleteAllDataConfirmation) {
Button("Delete", role: .destructive) {
quicknameViewModel.delete()
onDeleteAllData()
dismiss()
}

Button("Cancel") {
showingDeleteAllDataConfirmation = false
}
.selfSizingSheet(isPresented: $showingDeleteAllDataConfirmation) {
DeleteAllDataView(
viewModel: viewModel,
onComplete: {
dismiss()
onDeleteAllData()
}
)
.interactiveDismissDisabled(viewModel.isDeleting)
}
}
}
Expand Down Expand Up @@ -208,6 +208,10 @@ struct AppSettingsView: View {
#Preview {
let quicknameViewModel = QuicknameSettingsViewModel.shared
NavigationStack {
AppSettingsView(quicknameViewModel: quicknameViewModel) {}
AppSettingsView(
viewModel: .mock,
quicknameViewModel: quicknameViewModel,
onDeleteAllData: {}
)
}
}
50 changes: 50 additions & 0 deletions Convos/App Settings/AppSettingsViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import ConvosCore
import Foundation
import Observation

@MainActor
@Observable
final class AppSettingsViewModel {
// MARK: - State

private(set) var isDeleting: Bool = false
private(set) var deletionProgress: InboxDeletionProgress?
private(set) var deletionError: Error?

// MARK: - Dependencies

private let session: any SessionManagerProtocol

init(session: any SessionManagerProtocol) {
self.session = session
}

// MARK: - Actions

func deleteAllData(onComplete: @escaping () -> Void) {
guard !isDeleting else { return }
isDeleting = true
deletionError = nil
deletionProgress = nil

QuicknameSettingsViewModel.shared.delete()

Task {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Task in deleteAllData isn’t tracked. If this view model deallocates during deletion, onComplete() can still fire and drive UI on a dead screen. Consider storing the task and cancelling it in deinit or when appropriate to avoid running completion after teardown.

🚀 Reply to ask Macroscope to explain or update this suggestion.

👍 Helpful? React to give us feedback.

do {
for try await progress in session.deleteAllInboxesWithProgress() {
deletionProgress = progress
}
onComplete()
} catch {
deletionError = error
isDeleting = false
}
}
}
}

extension AppSettingsViewModel {
static var mock: AppSettingsViewModel {
AppSettingsViewModel(session: MockInboxesService())
}
}
103 changes: 103 additions & 0 deletions Convos/App Settings/DeleteAllDataView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import ConvosCore
import SwiftUI

struct DeleteAllDataView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@Bindable var viewModel: AppSettingsViewModel
let onComplete: () -> Void

var title: String {
"Delete everything?"
}

var subtitle: String {
"This will permanently delete all conversations on this device, as well as your Quickname."
}

var body: some View {
VStack(alignment: .leading, spacing: DesignConstants.Spacing.step4x) {
Text(title)
.font(.system(.largeTitle))
.fontWeight(.bold)
Text(subtitle)
.font(.body)
.foregroundStyle(.colorTextSecondary)

if let error = viewModel.deletionError {
Text("\(error.localizedDescription). Try again.")
.font(.subheadline)
.foregroundStyle(.colorTextSecondary)
.padding(.vertical, DesignConstants.Spacing.stepX)
}

VStack(spacing: DesignConstants.Spacing.step4x) {
HoldToDeleteButton(
isDeleting: viewModel.isDeleting,
onDelete: { deleteAllData() }
)
.hoverEffect(.lift)

Button {
dismiss()
} label: {
Text("Cancel")
}
.convosButtonStyle(.text)
.disabled(viewModel.isDeleting)
.hoverEffect(.lift)
}

.padding(.top, DesignConstants.Spacing.step4x)
}
.padding([.leading, .top, .trailing], DesignConstants.Spacing.step10x)
}

private func deleteAllData() {
viewModel.deleteAllData(onComplete: onComplete)
}
}

// MARK: - Hold To Delete Button

private struct HoldToDeleteButton: View {
let isDeleting: Bool
let onDelete: () -> Void

private var buttonConfig: HoldToConfirmStyleConfig {
var config = HoldToConfirmStyleConfig.default
config.duration = 3.0
config.backgroundColor = .colorCaution
return config
}

var body: some View {
Button {
onDelete()
} label: {
textView
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
}
.disabled(isDeleting)
.buttonStyle(HoldToConfirmPrimitiveStyle(config: buttonConfig))
}

private var textView: some View {
ZStack {
Text("Hold to delete")
.opacity(isDeleting ? 0 : 1)

Text("Deleting...")
.opacity(isDeleting ? 1 : 0)
}
.animation(.easeInOut(duration: 0.2), value: isDeleting)
}
}

#Preview {
let viewModel = AppSettingsViewModel(session: ConvosClient.mock().session)
DeleteAllDataView(
viewModel: viewModel,
onComplete: {}
)
}
Loading