Skip to content

Commit bba4024

Browse files
committed
Add maximum upload size
Add maximum upload size
1 parent d00a75f commit bba4024

File tree

6 files changed

+134
-49
lines changed

6 files changed

+134
-49
lines changed

Modules/Sources/Support/InternalDataProvider.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,8 @@ actor InternalUserDataProvider: CurrentUserDataProvider {
341341
}
342342

343343
actor InternalSupportConversationDataProvider: SupportConversationDataProvider {
344+
let maximumUploadSize: UInt64 = 5_000_000 // 5MB
345+
344346
private var conversations: [UInt64: Conversation] = [:]
345347

346348
nonisolated func loadSupportConversations() throws -> any CachedAndFetchedResult<[ConversationSummary]> {

Modules/Sources/Support/SupportDataProvider.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ public final class SupportDataProvider: ObservableObject, Sendable {
147147
}
148148
}
149149

150+
var maximumUploadSize: CGFloat {
151+
CGFloat(self.supportConversationDataProvider.maximumUploadSize)
152+
}
153+
150154
// Application Logs
151155
public func fetchApplicationLogs() async throws -> [ApplicationLog] {
152156
try await self.applicationLogProvider.fetchApplicationLogs()
@@ -264,6 +268,8 @@ public protocol BotConversationDataProvider: Actor {
264268
}
265269

266270
public protocol SupportConversationDataProvider: Actor {
271+
nonisolated var maximumUploadSize: UInt64 { get }
272+
267273
nonisolated func loadSupportConversations() throws -> any CachedAndFetchedResult<[ConversationSummary]>
268274
nonisolated func loadSupportConversation(id: UInt64) throws -> any CachedAndFetchedResult<Conversation>
269275

Modules/Sources/Support/UI/Support Conversations/ScreenshotPicker.swift

Lines changed: 116 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,23 @@ import PhotosUI
33

44
struct ScreenshotPicker: View {
55

6-
private let maxScreenshots = 5
6+
enum ViewState: Sendable {
7+
case ready
8+
case loading
9+
case error(Error)
10+
11+
var isLoadingMoreImages: Bool {
12+
guard case .loading = self else { return false }
13+
return true
14+
}
15+
16+
var error: Error? {
17+
guard case .error(let error) = self else { return nil }
18+
return error
19+
}
20+
}
21+
22+
private let maxScreenshots = 10
723

824
@State
925
private var selectedPhotos: [PhotosPickerItem] = []
@@ -12,78 +28,67 @@ struct ScreenshotPicker: View {
1228
private var attachedImages: [UIImage] = []
1329

1430
@State
15-
private var error: Error?
31+
private var state: ViewState = .ready
1632

1733
@Binding
1834
var attachedImageUrls: [URL]
1935

36+
@State
37+
private var currentUploadSize: CGFloat = 0
38+
39+
let maximumUploadSize: CGFloat?
40+
41+
@Binding
42+
var uploadLimitExceeded: Bool
43+
2044
var body: some View {
2145
Section {
22-
VStack(alignment: .leading, spacing: 12) {
23-
Text(Localization.screenshotsDescription)
24-
.font(.caption)
25-
.foregroundColor(.secondary)
26-
27-
// Screenshots display
28-
if !attachedImages.isEmpty {
29-
ScrollView(.horizontal, showsIndicators: false) {
30-
LazyHStack(spacing: 12) {
31-
ForEach(Array(attachedImages.enumerated()), id: \.offset) { index, image in
32-
ZStack(alignment: .topTrailing) {
33-
Image(uiImage: image)
34-
.resizable()
35-
.aspectRatio(contentMode: .fill)
36-
.frame(width: 80, height: 80)
37-
.clipped()
38-
.cornerRadius(8)
39-
40-
// Remove button
41-
Button {
42-
// attachedImages will be updated by changing `selectedPhotos`, but not immediately. This line is here to make the UI feel snappy
43-
attachedImages.remove(at: index)
44-
selectedPhotos.remove(at: index)
45-
} label: {
46-
Image(systemName: "xmark.circle.fill")
47-
.foregroundColor(.red)
48-
.background(Color.white, in: Circle())
49-
}
50-
.padding(4)
51-
}
52-
}
53-
}
54-
.padding(.horizontal, 2)
55-
}
56-
}
46+
Text(Localization.screenshotsDescription)
47+
.font(.body)
48+
.foregroundColor(.secondary)
5749

58-
if let error {
50+
if let error = self.state.error {
5951
ErrorView(
6052
title: "Unable to load screenshot",
6153
message: error.localizedDescription
62-
).frame(maxWidth: .infinity)
54+
)
55+
}
56+
57+
if !attachedImages.isEmpty {
58+
imageGallery
59+
maxSizeIndicator
6360
}
6461

6562
// Add screenshots button
6663
PhotosPicker(
6764
selection: $selectedPhotos,
6865
maxSelectionCount: maxScreenshots,
6966
matching: .images
70-
) { [imageCount = attachedImages.count] in
67+
) { [imageCount = attachedImages.count, isLoading = self.state.isLoadingMoreImages, uploadLimitExceeded = self.uploadLimitExceeded] in
7168
HStack {
72-
Image(systemName: "camera.fill")
69+
if isLoading {
70+
ProgressView()
71+
.tint(Color.accentColor)
72+
} else {
73+
Image(systemName: "camera.fill")
74+
}
75+
7376
Text(imageCount == 0 ? Localization.addScreenshots : Localization.addMoreScreenshots)
7477
}
7578
.frame(maxWidth: .infinity)
7679
.padding()
7780
.background(Color.accentColor.opacity(0.1))
78-
.foregroundColor(Color.accentColor)
81+
.foregroundStyle(uploadLimitExceeded ? Color.gray : Color.accentColor)
7982
.cornerRadius(8)
8083
}
8184
.onChange(of: selectedPhotos) { _, newItems in
8285
Task {
86+
self.state = .loading
8387
await loadSelectedPhotos(newItems)
88+
self.state = .ready
8489
}
8590
}
86-
}
91+
.disabled(uploadLimitExceeded)
8792
} header: {
8893
HStack {
8994
Text(Localization.screenshots)
@@ -92,32 +97,92 @@ struct ScreenshotPicker: View {
9297
.foregroundColor(.secondary)
9398
}
9499
}
100+
.listRowSeparator(.hidden)
101+
.selectionDisabled()
102+
}
103+
104+
@ViewBuilder
105+
var imageGallery: some View {
106+
// Screenshots display
107+
ScrollView(.horizontal, showsIndicators: false) {
108+
LazyHStack(spacing: 12) {
109+
ForEach(Array(attachedImages.enumerated()), id: \.offset) { index, image in
110+
ZStack(alignment: .topTrailing) {
111+
Image(uiImage: image)
112+
.resizable()
113+
.aspectRatio(contentMode: .fill)
114+
.frame(width: 80, height: 80)
115+
.clipped()
116+
.cornerRadius(8)
117+
118+
// Remove button
119+
Button {
120+
// attachedImages will be updated by changing `selectedPhotos`, but not immediately. This line is here to make the UI feel snappy
121+
attachedImages.remove(at: index)
122+
selectedPhotos.remove(at: index)
123+
} label: {
124+
Image(systemName: "xmark.circle.fill")
125+
.foregroundColor(.red)
126+
.background(Color.white, in: Circle())
127+
}
128+
.padding(4)
129+
}
130+
}
131+
}
132+
.padding(.horizontal, 2)
133+
}
134+
}
135+
136+
@ViewBuilder
137+
var maxSizeIndicator: some View {
138+
if let maximumUploadSize {
139+
VStack(alignment: .leading) {
140+
ProgressView(value: currentUploadSize, total: maximumUploadSize)
141+
.tint(uploadLimitExceeded ? Color.red : Color.accentColor)
142+
143+
Text("Attachment Limit: \(format(bytes: currentUploadSize)) / \(format(bytes: maximumUploadSize))")
144+
.font(.caption2)
145+
.foregroundStyle(Color.secondary)
146+
}
147+
}
148+
}
149+
150+
private func format(bytes: CGFloat) -> String {
151+
ByteCountFormatter().string(fromByteCount: Int64(bytes))
95152
}
96153

97154
/// Loads selected photos from PhotosPicker
98155
@MainActor
99156
func loadSelectedPhotos(_ items: [PhotosPickerItem]) async {
100157
var newImages: [UIImage] = []
101158
var newUrls: [URL] = []
159+
var totalSize: CGFloat = 0
102160

103161
do {
104162
for item in items {
105163
if let data = try await item.loadTransferable(type: Data.self) {
106164
if let image = UIImage(data: data) {
107165
newImages.append(image)
108166
}
167+
168+
totalSize += CGFloat(data.count)
109169
}
110170

111171
if let file = try await item.loadTransferable(type: ScreenshotFile.self) {
112172
newUrls.append(file.url)
113173
}
114174
}
115175

116-
attachedImages = newImages
117-
attachedImageUrls = newUrls
176+
self.attachedImages = newImages
177+
self.attachedImageUrls = newUrls
178+
179+
withAnimation {
180+
self.currentUploadSize = totalSize
181+
self.uploadLimitExceeded = totalSize > maximumUploadSize ?? .infinity
182+
}
118183
} catch {
119184
withAnimation {
120-
self.error = error
185+
self.state = .error(error)
121186
}
122187
}
123188
}
@@ -159,7 +224,11 @@ struct ScreenshotFile: Transferable {
159224

160225
var body: some View {
161226
Form {
162-
ScreenshotPicker(attachedImageUrls: $selectedPhotoUrls)
227+
ScreenshotPicker(
228+
attachedImageUrls: $selectedPhotoUrls,
229+
maximumUploadSize: 10_000_000,
230+
uploadLimitExceeded: .constant(false)
231+
)
163232
}
164233
.environmentObject(SupportDataProvider.testing)
165234
}

Modules/Sources/Support/UI/Support Conversations/SupportConversationReplyView.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@ public struct SupportConversationReplyView: View {
9797
}
9898

9999
ScreenshotPicker(
100-
attachedImageUrls: self.$selectedPhotos
100+
attachedImageUrls: self.$selectedPhotos,
101+
maximumUploadSize: self.dataProvider.maximumUploadSize,
102+
uploadLimitExceeded: self.$uploadLimitExceeded
101103
)
102104

103105
ApplicationLogPicker(

Modules/Sources/Support/UI/Support Conversations/SupportForm.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,14 @@ public struct SupportForm: View {
4646
@State private var applicationLogs: [ApplicationLog]
4747

4848
@State private var selectedPhotos: [URL] = []
49+
@State private var uploadLimitExceeded = false
4950

5051
/// UI State
5152
@State private var showLoadingIndicator = false
5253
@State private var shouldShowErrorAlert = false
5354
@State private var shouldShowSuccessAlert = false
5455
@State private var errorMessage = ""
56+
@State private var isDisplayingCancellationConfirmation: Bool = false
5557

5658
/// Callback for when form is dismissed
5759
public var onDismiss: (() -> Void)?
@@ -101,7 +103,9 @@ public struct SupportForm: View {
101103

102104
// Screenshots Section
103105
ScreenshotPicker(
104-
attachedImageUrls: $selectedPhotos
106+
attachedImageUrls: $selectedPhotos,
107+
maximumUploadSize: self.dataProvider.maximumUploadSize,
108+
uploadLimitExceeded: self.$uploadLimitExceeded
105109
)
106110

107111
// Application Logs Section

WordPress/Classes/ViewRelated/NewSupport/SupportDataProvider.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,8 @@ actor WpCurrentUserDataProvider: CurrentUserDataProvider {
257257

258258
actor WpSupportConversationDataProvider: SupportConversationDataProvider {
259259

260+
let maximumUploadSize: UInt64 = 30_000_000 // 30MB
261+
260262
private let wpcomClient: WordPressDotComClient
261263

262264
init(wpcomClient: WordPressDotComClient) {

0 commit comments

Comments
 (0)