diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index f71ef1439..09e7bf954 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -12,6 +12,7 @@ import com.vitorpamplona.amethyst.service.model.ContactListEvent import com.vitorpamplona.amethyst.service.model.Event import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent import com.vitorpamplona.amethyst.service.model.LnZapEvent +import com.vitorpamplona.amethyst.ui.actions.ServersAvailable import com.vitorpamplona.amethyst.ui.note.Nip47URI import fr.acinq.secp256k1.Hex import nostr.postr.Persona @@ -44,6 +45,7 @@ private object PrefKeys { const val TRANSLATE_TO = "translateTo" const val ZAP_AMOUNTS = "zapAmounts" const val DEFAULT_ZAPTYPE = "defaultZapType" + const val DEFAULT_FILE_SERVER = "defaultFileServer" const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer" const val LATEST_CONTACT_LIST = "latestContactList" const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog" @@ -194,6 +196,7 @@ object LocalPreferences { putString(PrefKeys.TRANSLATE_TO, account.translateTo) putString(PrefKeys.ZAP_AMOUNTS, gson.toJson(account.zapAmountChoices)) putString(PrefKeys.DEFAULT_ZAPTYPE, gson.toJson(account.defaultZapType)) + putString(PrefKeys.DEFAULT_FILE_SERVER, gson.toJson(account.defaultFileServer)) putString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, gson.toJson(account.zapPaymentRequest)) putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList)) putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog) @@ -225,6 +228,11 @@ object LocalPreferences { object : TypeToken() {}.type ) ?: LnZapEvent.ZapType.PUBLIC + val defaultFileServer = gson.fromJson( + getString(PrefKeys.DEFAULT_FILE_SERVER, "IMGUR"), + object : TypeToken() {}.type + ) ?: ServersAvailable.IMGUR + val zapPaymentRequestServer = try { getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let { gson.fromJson(it, Nip47URI::class.java) @@ -269,6 +277,7 @@ object LocalPreferences { translateTo, zapAmountChoices, defaultZapType, + defaultFileServer, zapPaymentRequestServer, hideDeleteRequestDialog, hideBlockAlertDialog, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index e09112dae..428656963 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -11,6 +11,7 @@ import com.vitorpamplona.amethyst.service.relays.Constants import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.RelayPool +import com.vitorpamplona.amethyst.ui.actions.ServersAvailable import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.note.Nip47URI import kotlinx.coroutines.DelicateCoroutinesApi @@ -45,6 +46,7 @@ class Account( var translateTo: String = Locale.getDefault().language, var zapAmountChoices: List = listOf(500L, 1000L, 5000L), var defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PRIVATE, + var defaultFileServer: ServersAvailable = ServersAvailable.IMGUR, var zapPaymentRequest: Nip47URI? = null, var hideDeleteRequestDialog: Boolean = false, var hideBlockAlertDialog: Boolean = false, @@ -652,6 +654,12 @@ class Account( saveable.invalidateData() } + fun changeDefaultFileServer(server: ServersAvailable) { + defaultFileServer = server + live.invalidateData() + saveable.invalidateData() + } + fun changeZapAmounts(newAmounts: List) { zapAmountChoices = newAmounts live.invalidateData() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt index 485f21062..ba62883c4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt @@ -7,13 +7,17 @@ import android.media.MediaScannerConnection import android.os.Build import android.os.Environment import android.provider.MediaStore +import android.webkit.MimeTypeMap import androidx.annotation.RequiresApi import com.vitorpamplona.amethyst.BuildConfig import okhttp3.* import okio.BufferedSource import okio.IOException +import okio.buffer import okio.sink +import okio.source import java.io.File +import java.util.UUID object ImageSaver { /** @@ -74,6 +78,38 @@ object ImageSaver { }) } + fun saveImage( + byteArray: ByteArray, + mimeType: String?, + context: Context, + onSuccess: () -> Any?, + onError: (Throwable) -> Any? + ) { + try { + val extension = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: "" + val buffer = byteArray.inputStream().source().buffer() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + saveContentQ( + displayName = UUID.randomUUID().toString(), + contentType = mimeType ?: "", + contentSource = buffer, + contentResolver = context.contentResolver + ) + } else { + saveContentDefault( + fileName = UUID.randomUUID().toString() + ".$extension", + contentSource = buffer, + context = context + ) + } + onSuccess() + } catch (e: Exception) { + e.printStackTrace() + onError(e) + } + } + @RequiresApi(Build.VERSION_CODES.Q) private fun saveContentQ( displayName: String, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt index cf15b8c3a..ffc78709c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt @@ -14,6 +14,7 @@ import java.util.* object ImageUploader { fun uploadImage( uri: Uri, + server: ServersAvailable, contentResolver: ContentResolver, onSuccess: (String, String?) -> Unit, onError: (Throwable) -> Unit diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 37904090f..e99552cd1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -58,6 +58,7 @@ import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.ui.components.* import com.vitorpamplona.amethyst.ui.note.ReplyInformation import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import kotlinx.coroutines.Dispatchers @@ -206,8 +207,10 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n if (url != null) { ImageVideoDescription( url, - onAdd = { description -> - postViewModel.upload(url, description, context) + account.defaultFileServer, + onAdd = { description, server -> + postViewModel.upload(url, description, server, context) + account.changeDefaultFileServer(server) }, onCancel = { postViewModel.contentToAddUrl = null @@ -485,10 +488,15 @@ fun SearchButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier } } +enum class ServersAvailable() { + IMGUR +} + @Composable fun ImageVideoDescription( uri: Uri, - onAdd: (String) -> Unit, + defaultServer: ServersAvailable, + onAdd: (String, ServersAvailable) -> Unit, onCancel: () -> Unit ) { val resolver = LocalContext.current.contentResolver @@ -498,6 +506,15 @@ fun ImageVideoDescription( val isImage = mediaType.startsWith("image") val isVideo = mediaType.startsWith("video") + val fileServers = listOf( + Pair(ServersAvailable.IMGUR, "imgur.com") + ) + + val fileServerOptions = fileServers.map { it.second } + + var selectedServer by remember { mutableStateOf(defaultServer) } + var message by remember { mutableStateOf("") } + Column( modifier = Modifier .fillMaxWidth() @@ -595,30 +612,49 @@ fun ImageVideoDescription( } } - var message by remember { mutableStateOf("") } - - OutlinedTextField( - label = { Text(text = stringResource(R.string.content_description)) }, - modifier = Modifier.fillMaxWidth(), - value = message, - onValueChange = { message = it }, - placeholder = { - Text( - text = stringResource(R.string.content_description_example), - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) - ) - }, - keyboardOptions = KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.Sentences + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + TextSpinner( + label = stringResource(id = R.string.file_server), + placeholder = fileServers.filter { it.first == defaultServer }.first().second, + options = fileServerOptions, + onSelect = { + selectedServer = fileServers[it].first + }, + modifier = Modifier + .weight(1f) ) - ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + OutlinedTextField( + label = { Text(text = stringResource(R.string.content_description)) }, + modifier = Modifier.fillMaxWidth(), + value = message, + onValueChange = { message = it }, + placeholder = { + Text( + text = stringResource(R.string.content_description_example), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences + ) + ) + } Button( modifier = Modifier .fillMaxWidth() .padding(vertical = 10.dp), onClick = { - onAdd(message) + onAdd(message, selectedServer) }, shape = RoundedCornerShape(15.dp), colors = ButtonDefaults.buttonColors( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index a07701358..77ff7a0ac 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -110,12 +110,13 @@ open class NewPostViewModel : ViewModel() { cancel() } - fun upload(it: Uri, description: String, context: Context) { + fun upload(it: Uri, description: String, server: ServersAvailable, context: Context) { isUploadingImage = true contentToAddUrl = null ImageUploader.uploadImage( uri = it, + server = server, contentResolver = context.contentResolver, onSuccess = { imageUrl, mimeType -> createNIP97Record(imageUrl, mimeType, description) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt index 5a252ef02..269f22871 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt @@ -170,6 +170,7 @@ class NewUserMetadataViewModel : ViewModel() { ImageUploader.uploadImage( uri = it, + server = account.defaultFileServer, contentResolver = context.contentResolver, onSuccess = { imageUrl, mimeType -> onUploading(false) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index fe3602a31..63e0e4d92 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -233,7 +233,7 @@ fun ChannelScreen( tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), modifier = Modifier.padding(start = 5.dp) ) { - channelScreenModel.upload(it, "", context) + channelScreenModel.upload(it, "", account.defaultFileServer, context) } }, colors = TextFieldDefaults.textFieldColors( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index 5dd3bb2ca..cf75e066a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -194,7 +194,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), modifier = Modifier.padding(start = 5.dp) ) { - chatRoomScreenModel.upload(it, "", context) + chatRoomScreenModel.upload(it, "", account.defaultFileServer, context) } }, colors = TextFieldDefaults.textFieldColors( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bbab09bf3..5fb553efd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -303,7 +303,8 @@ Add Image Add Video Add Document - Create and Add + + Add to Message Description of the contents A blue boat in a white sandy beach at sunset @@ -322,4 +323,7 @@ Non-Zap No trace in Nostr, only in Lightning + + File Server +