1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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
))
}
}