Skip to content

Commit

Permalink
Merge pull request #1321 from planetary-social/large_file_upload_error
Browse files Browse the repository at this point in the history
Fixes alerts when uploading big files and suggests users subscribe to nostr.build
  • Loading branch information
pelumy authored Sep 17, 2024
2 parents 542e1bb + d15cdf4 commit 2b3a70d
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed a bug where toggles in the settings screen were white instead of green when toggled on. [#1251](https://github.com/planetary-social/nos/issues/1251)
- Added routing to profile when tapping on follow notification. [#1447](https://github.com/planetary-social/nos/issues/1447)
- Localized follows notifications. [#1446](https://github.com/planetary-social/nos/issues/1446)
- Fixed alert when uploading big files suggesting users pay for nostr.build. [#1321](https://github.com/planetary-social/nos/issues/1321)

### Internal Changes
- Use NIP-92 media metadata to display media in the proper orientation. Currently behind the “Enable new media display” feature flag. [#1172](https://github.com/planetary-social/nos/issues/1172)
Expand Down
117 changes: 117 additions & 0 deletions Nos/Assets/Localization/ImagePicker.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
"value" : "Camera"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Cámara"
}
},
"fa" : {
"stringUnit" : {
"state" : "translated",
Expand Down Expand Up @@ -57,6 +63,12 @@
"value" : "Camera is not available on this device"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "La cámara no esta disponible en este aparato"
}
},
"fa" : {
"stringUnit" : {
"state" : "translated",
Expand Down Expand Up @@ -98,6 +110,12 @@
"value" : "Error uploading the file"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Error subiendo el archivo"
}
},
"fa" : {
"stringUnit" : {
"state" : "translated",
Expand All @@ -124,6 +142,40 @@
}
}
},
"errorUploadingFileExceedsLimit" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "The max file size for free media uploads is %@. To upload large files, upgrade to a Nostr.build Professional account."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "El maximo tamaño para publicacion de achivos gratis es %@. Para subir archives mas grande puedes pagar para subscribir a una cuenta Nostr.build pro."
}
}
}
},
"errorUploadingFileExceedsSizeLimit" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Get a Nostr.build account to upload larger files"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Registarse para una cuenta pro de Nostr.build para la abilidad de subir archivos mas grande"
}
}
}
},
"errorUploadingFileMessage" : {
"extractionState" : "manual",
"localizations" : {
Expand All @@ -139,6 +191,12 @@
"value" : "An error was encountered when uploading the file you provided. Please try again."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hubo un error en el intento de subir archivos. Porfavor intenta de vuelta."
}
},
"fa" : {
"stringUnit" : {
"state" : "translated",
Expand Down Expand Up @@ -173,6 +231,29 @@
"state" : "translated",
"value" : "An error was encountered when uploading the file you provided. The message was: \"%@\""
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hubo un error en el intento de subir el archivo. El mensaje de error era\"%@\""
}
}
}
},
"getAccount" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Get Account"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Registar una Cuenta"
}
}
}
},
Expand All @@ -191,6 +272,12 @@
"value" : "You can allow camera permissions by opening the Settings app."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Puedes dar permiso a aceder la cámara en el app de configuraciones."
}
},
"fa" : {
"stringUnit" : {
"state" : "translated",
Expand Down Expand Up @@ -232,6 +319,12 @@
"value" : "Permissions required for %@"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Prescisa permiso para %@"
}
},
"fa" : {
"stringUnit" : {
"state" : "translated",
Expand Down Expand Up @@ -273,6 +366,12 @@
"value" : "Photo Library"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bibiloteca de Fotos"
}
},
"fa" : {
"stringUnit" : {
"state" : "translated",
Expand Down Expand Up @@ -314,6 +413,12 @@
"value" : "Select from Photo Library"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Elijir fotos en la biblioteca"
}
},
"fa" : {
"stringUnit" : {
"state" : "translated",
Expand Down Expand Up @@ -366,6 +471,12 @@
"value" : "Take photo or video"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sacar un foto o video"
}
},
"fa" : {
"stringUnit" : {
"state" : "needs_review",
Expand Down Expand Up @@ -407,6 +518,12 @@
"value" : "Uploading..."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Subiendo... "
}
},
"fa" : {
"stringUnit" : {
"state" : "translated",
Expand Down
41 changes: 33 additions & 8 deletions Nos/Service/FileStorage/FileStorageAPIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ enum FileStorageAPIClientError: Error {
case invalidResponseURL(String)
case invalidURLRequest
case missingKeyPair
case fileTooBig(String?)
case uploadFailed(String?)
}

Expand Down Expand Up @@ -53,8 +54,9 @@ class NostrBuildAPIClient: FileStorageAPIClient {
assert(fileURL.isFileURL, "The URL must point to a file.")
let apiURL = try await apiURL()
let (request, data) = try uploadRequest(fileAt: fileURL, isProfilePhoto: isProfilePhoto, apiURL: apiURL)
let (responseData, _) = try await URLSession.shared.upload(for: request, from: data)
return try assetURL(from: responseData)
let (responseData, response) = try await URLSession.shared.upload(for: request, from: data)
/// Attempt to retrieve the asset URL from the response data and HTTP response.
return try assetURL(from: responseData, response: response as? HTTPURLResponse)
}

// MARK: - Internal
Expand All @@ -74,19 +76,29 @@ class NostrBuildAPIClient: FileStorageAPIClient {
}

/// The URL of the uploaded asset parsed from the API's response.
private func assetURL(from responseData: Data) throws -> URL {
let response = try decoder.decode(FileStorageUploadResponseJSON.self, from: responseData)
guard let urlString = response.nip94Event?.urlString else {
throw FileStorageAPIClientError.uploadFailed(response.message)
private func assetURL(from responseData: Data, response: HTTPURLResponse?) throws -> URL {
let decodedResponse = try decoder.decode(FileStorageUploadResponseJSON.self, from: responseData)

guard let urlString = decodedResponse.nip94Event?.urlString else {
// Assign an empty string if the response message is nil.
let message = decodedResponse.message ?? ""

// Checks if the response contains a status code of `413 Payload Too Large`.
if let errorCode = response?.statusCode, errorCode == 413 {
// Verify if the error message indicates the file size exceeds the limit.
let fileSizeLimit = fileSizeLimit(from: message)
throw FileStorageAPIClientError.fileTooBig(fileSizeLimit)
}
// Throw an error indicating the upload failed with the provided message.
throw FileStorageAPIClientError.uploadFailed(message)
}

guard let url = URL(string: urlString) else {
throw FileStorageAPIClientError.invalidResponseURL(urlString)
}
return url
}

// MARK: - Internal

/// Fetches server info from the file storage API.
/// - Returns: the decoded JSON containing server info for the file storage API.
func fetchServerInfo() async throws -> FileStorageServerInfoResponseJSON {
Expand All @@ -102,6 +114,19 @@ class NostrBuildAPIClient: FileStorageAPIClient {
throw FileStorageAPIClientError.decodingError
}
}

/// Gets the file size limit from the error message.
/// - Parameter message: The error message from nostr.build.
/// - Returns: The file size limit from the error message.
func fileSizeLimit(from message: String) -> String? {
let pattern = /File size exceeds the limit of (\d*\.\d* [MKGT]B)/

guard let match = message.firstMatch(of: pattern) else {
return nil
}

return String(match.1)
}

/// Creates a URLRequest and Data from a file URL to be uploaded to the file storage API.
func uploadRequest(fileAt fileURL: URL, isProfilePhoto: Bool, apiURL: URL) throws -> (URLRequest, Data) {
Expand Down
66 changes: 53 additions & 13 deletions Nos/Views/NoteComposer/ComposerActionBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ struct ComposerActionBar: View {
@State private var alert: AlertState<AlertAction>?

fileprivate enum AlertAction {
case cancel
case getAccount
}

var backArrow: some View {
Expand Down Expand Up @@ -68,7 +70,15 @@ struct ComposerActionBar: View {
.onChange(of: expirationTime) { _, _ in
subMenu = .none
}
.alert(unwrapping: $alert) { (_: AlertAction?) in
.alert(unwrapping: $alert) { action in
switch action {
case .getAccount:
if let url = URL(string: "https://nostr.build/plans/") {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
default:
break
}
}
.background(
LinearGradient(
Expand Down Expand Up @@ -152,7 +162,9 @@ struct ComposerActionBar: View {

/// Uploads an image at the given URL to a file storage service.
/// - Parameter imageURL: File URL of the image the user wants to upload.
private func uploadImage(at imageURL: URL) async {
private func uploadImage(
at imageURL: URL
) async {
do {
startUploadingImage()
let url = try await fileStorageAPIClient.upload(fileAt: imageURL, isProfilePhoto: false)
Expand All @@ -161,17 +173,7 @@ struct ComposerActionBar: View {
} catch {
endUploadingImage()

alert = AlertState {
TextState(String(localized: .imagePicker.errorUploadingFile))
} message: {
if case let FileStorageAPIClientError.uploadFailed(message) = error, let message {
TextState(
String(localized: .imagePicker.errorUploadingFileWithMessage(message))
)
} else {
TextState(String(localized: .imagePicker.errorUploadingFileMessage))
}
}
alert = createAlert(for: error)
}
}

Expand All @@ -183,6 +185,44 @@ struct ComposerActionBar: View {
self.isUploadingImage = false
self.subMenu = .none
}

/// Creates an alert based on the error
private func createAlert(
for error: Error
) -> AlertState<ComposerActionBar.AlertAction> {
var title = String(localized: .imagePicker.errorUploadingFile)
var message: String
var buttons: [ButtonState<ComposerActionBar.AlertAction>] = [
.default(
TextState(String(localized: .localizable.ok)),
action: .send(.cancel)
)
]

if case let FileStorageAPIClientError.fileTooBig(errorMessage) = error, let errorMessage {
title = String(localized: .imagePicker.errorUploadingFileExceedsSizeLimit)
message = String(localized: .imagePicker.errorUploadingFileExceedsLimit(errorMessage))
buttons = [
.cancel(
TextState(String(localized: .localizable.cancel)), action: .send(.cancel)
),
.default(
TextState(String(localized: .imagePicker.getAccount)),
action: .send(.getAccount)
)
]
} else if case let FileStorageAPIClientError.uploadFailed(errorMessage) = error, let errorMessage {
message = String(localized: .imagePicker.errorUploadingFileWithMessage(errorMessage))
} else {
message = String(localized: .imagePicker.errorUploadingFileMessage)
}

return AlertState(
title: TextState(title),
message: TextState(message),
buttons: buttons
)
}
}

struct ComposerActionBar_Previews: PreviewProvider {
Expand Down
Loading

0 comments on commit 2b3a70d

Please sign in to comment.