~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-ios

1 collaborator
0
1
main
133 lines · 4.9 KB Raw History
import SwiftUI

/// What a gallery card can put on screen over the list it sits in.
///
/// Every screen showing a card needs the same five: the story viewer, the
/// comment sheet, the report sheet, the delete confirmation and the alert when
/// a delete fails. Six screens each kept six `@State` properties and ~78 lines
/// of presenters to do it, identical apart from which collection to update
/// afterwards.
@MainActor
@Observable
final class GalleryCardModals {
    var commentUri: String?
    var report: GrainGallery?
    var storyAuthor: GrainStoryAuthor?
    var pendingDeleteUri: String?
    var isConfirmingDelete = false
    var deleteError: String?

    init() {}

    /// Ask before deleting `uri`. The confirmation is what actually deletes.
    func confirmDelete(_ uri: String) {
        pendingDeleteUri = uri
        isConfirmingDelete = true
    }
}

private struct GalleryCardModalsModifier: ViewModifier {
    @Environment(AuthManager.self) private var auth
    @Environment(Router.self) private var router
    let client: XRPCClient
    let modals: GalleryCardModals
    /// The gallery's own comment count lives in whichever collection the screen
    /// is showing, so updating it stays with the screen.
    let onCommentCountChanged: (String, Int) -> Void
    let onDeleted: (String) -> Void

    func body(content: Content) -> some View {
        @Bindable var modals = modals
        return content
            .fullScreenCover(item: $modals.storyAuthor) { author in
                StoryViewer(
                    authors: [author],
                    client: client,
                    onProfileTap: { did in
                        modals.storyAuthor = nil
                        router.push(.profile(did: did))
                    },
                    onDismiss: { modals.storyAuthor = nil }
                )
                .environment(auth)
            }
            .sheet(isPresented: Binding(
                get: { modals.commentUri != nil },
                set: {
                    if !$0 {
                        modals.commentUri = nil
                    }
                }
            )) {
                if let uri = modals.commentUri {
                    CommentSheetView(
                        client: client,
                        galleryUri: uri,
                        onDismiss: { modals.commentUri = nil },
                        onProfileTap: { did in
                            modals.commentUri = nil
                            router.push(.profile(did: did))
                        },
                        onHashtagTap: { tag in
                            modals.commentUri = nil
                            router.push(.hashtag(tag))
                        },
                        onStoryTap: { author in
                            modals.commentUri = nil
                            modals.storyAuthor = author
                        },
                        onCommentCountChanged: { onCommentCountChanged(uri, $0) }
                    )
                }
            }
            .sheet(item: $modals.report) { gallery in
                ReportView(client: client, subjectUri: gallery.uri, subjectCid: gallery.cid)
            }
            .alert("Delete gallery?", isPresented: $modals.isConfirmingDelete) {
                Button("Delete", role: .destructive) {
                    guard let uri = modals.pendingDeleteUri else { return }
                    modals.pendingDeleteUri = nil
                    Task {
                        switch await GalleryService.delete(galleryUri: uri, client: client, auth: auth) {
                        case .success:
                            onDeleted(uri)
                        case let .failure(error):
                            modals.deleteError = error.localizedDescription
                        }
                    }
                }
                Button("Cancel", role: .cancel) { modals.pendingDeleteUri = nil }
            } message: {
                Text("This will permanently delete this gallery and all its photos.")
            }
            .alert("Couldn't delete gallery", isPresented: Binding(
                get: { modals.deleteError != nil },
                set: {
                    if !$0 {
                        modals.deleteError = nil
                    }
                }
            )) {
                Button("OK", role: .cancel) { modals.deleteError = nil }
            } message: {
                Text(modals.deleteError ?? "")
            }
    }
}

extension View {
    /// Attach the modals a gallery card can raise.
    func galleryCardModals(
        client: XRPCClient,
        modals: GalleryCardModals,
        onCommentCountChanged: @escaping (String, Int) -> Void,
        onDeleted: @escaping (String) -> Void
    ) -> some View {
        modifier(GalleryCardModalsModifier(
            client: client,
            modals: modals,
            onCommentCountChanged: onCommentCountChanged,
            onDeleted: onDeleted
        ))
    }
}