diff --git a/amethyst/src/main/AndroidManifest.xml b/amethyst/src/main/AndroidManifest.xml
index dfd0f2df5..049a2f5ad 100644
--- a/amethyst/src/main/AndroidManifest.xml
+++ b/amethyst/src/main/AndroidManifest.xml
@@ -103,6 +103,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt
index 91aedb7cb..c18608e6a 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt
@@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.ui.navigation
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
+import android.net.Uri
+import android.os.Parcelable
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -291,6 +293,11 @@ fun AppNavigation(
popEnterTransition = { scaleIn },
popExitTransition = { slideOutVerticallyToBottom },
) {
+ val draftMessage = it.message()
+ val attachment =
+ it.arguments?.getString("attachment")?.ifBlank { null }?.let {
+ Uri.parse(it)
+ }
val baseReplyTo = it.arguments?.getString("baseReplyTo")
val quote = it.arguments?.getString("quote")
val fork = it.arguments?.getString("fork")
@@ -298,6 +305,8 @@ fun AppNavigation(
val draft = it.arguments?.getString("draft")
val enableMessageInterface = it.arguments?.getBoolean("enableMessageInterface") ?: false
NewPostScreen(
+ message = draftMessage,
+ attachment = attachment,
baseReplyTo = baseReplyTo?.let { hex -> accountViewModel.getNoteIfExists(hex) },
quote = quote?.let { hex -> accountViewModel.getNoteIfExists(hex) },
fork = fork?.let { hex -> accountViewModel.getNoteIfExists(hex) },
@@ -326,86 +335,106 @@ private fun NavigateIfIntentRequested(
val activity = LocalContext.current.getActivity()
var newAccount by remember { mutableStateOf(null) }
- var currentIntentNextPage by remember {
- mutableStateOf(
- activity.intent
- ?.data
- ?.toString()
- ?.ifBlank { null },
- )
- }
-
- currentIntentNextPage?.let { intentNextPage ->
- var actionableNextPage by remember {
- mutableStateOf(uriToRoute(intentNextPage))
+ if (activity.intent.action == Intent.ACTION_SEND) {
+ activity.intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
+ nav.newStack(buildNewPostRoute(draftMessage = it))
}
- LaunchedEffect(intentNextPage) {
- if (actionableNextPage != null) {
- actionableNextPage?.let {
- val currentRoute = getRouteWithArguments(nav.controller)
- if (!isSameRoute(currentRoute, it)) {
- nav.newStack(it)
- }
- actionableNextPage = null
- }
- } else if (intentNextPage.contains("ncryptsec1")) {
- // login functions
- Nip19Bech32.tryParseAndClean(intentNextPage)?.let {
- newAccount = it
- }
+ (activity.intent.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri)?.let {
+ nav.newStack(buildNewPostRoute(attachment = it))
+ }
+ } else {
+ var currentIntentNextPage by remember {
+ mutableStateOf(
+ activity.intent
+ ?.data
+ ?.toString()
+ ?.ifBlank { null },
+ )
+ }
- actionableNextPage = null
- } else {
- accountViewModel.toast(
- R.string.invalid_nip19_uri,
- R.string.invalid_nip19_uri_description,
- intentNextPage,
- )
+ currentIntentNextPage?.let { intentNextPage ->
+ var actionableNextPage by remember {
+ mutableStateOf(uriToRoute(intentNextPage))
}
- currentIntentNextPage = null
- }
- }
-
- val scope = rememberCoroutineScope()
-
- DisposableEffect(nav, activity) {
- val consumer =
- Consumer { intent ->
- val uri = intent.data?.toString()
- if (!uri.isNullOrBlank()) {
- // navigation functions
- val newPage = uriToRoute(uri)
-
- if (newPage != null) {
+ LaunchedEffect(intentNextPage) {
+ if (actionableNextPage != null) {
+ actionableNextPage?.let {
val currentRoute = getRouteWithArguments(nav.controller)
- if (!isSameRoute(currentRoute, newPage)) {
- nav.newStack(newPage)
+ if (!isSameRoute(currentRoute, it)) {
+ nav.newStack(it)
}
- } else if (uri.contains("ncryptsec")) {
- // login functions
- Nip19Bech32.tryParseAndClean(uri)?.let {
- newAccount = it
+ actionableNextPage = null
+ }
+ } else if (intentNextPage.contains("ncryptsec1")) {
+ // login functions
+ Nip19Bech32.tryParseAndClean(intentNextPage)?.let {
+ newAccount = it
+ }
+
+ actionableNextPage = null
+ } else {
+ accountViewModel.toast(
+ R.string.invalid_nip19_uri,
+ R.string.invalid_nip19_uri_description,
+ intentNextPage,
+ )
+ }
+
+ currentIntentNextPage = null
+ }
+ }
+
+ val scope = rememberCoroutineScope()
+
+ DisposableEffect(nav, activity) {
+ val consumer =
+ Consumer { intent ->
+ if (intent.action == Intent.ACTION_SEND) {
+ intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
+ nav.newStack(buildNewPostRoute(draftMessage = it))
+ }
+
+ (intent.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri)?.let {
+ nav.newStack(buildNewPostRoute(attachment = it))
}
} else {
- scope.launch {
- delay(1000)
- accountViewModel.toast(
- R.string.invalid_nip19_uri,
- R.string.invalid_nip19_uri_description,
- uri,
- )
+ val uri = intent.data?.toString()
+ if (!uri.isNullOrBlank()) {
+ // navigation functions
+ val newPage = uriToRoute(uri)
+
+ if (newPage != null) {
+ val currentRoute = getRouteWithArguments(nav.controller)
+ if (!isSameRoute(currentRoute, newPage)) {
+ nav.newStack(newPage)
+ }
+ } else if (uri.contains("ncryptsec")) {
+ // login functions
+ Nip19Bech32.tryParseAndClean(uri)?.let {
+ newAccount = it
+ }
+ } else {
+ scope.launch {
+ delay(1000)
+ accountViewModel.toast(
+ R.string.invalid_nip19_uri,
+ R.string.invalid_nip19_uri_description,
+ uri,
+ )
+ }
+ }
}
}
}
- }
- activity.addOnNewIntentListener(consumer)
- onDispose { activity.removeOnNewIntentListener(consumer) }
- }
+ activity.addOnNewIntentListener(consumer)
+ onDispose { activity.removeOnNewIntentListener(consumer) }
+ }
- if (newAccount != null) {
- AddAccountDialog(newAccount, accountStateViewModel) { newAccount = null }
+ if (newAccount != null) {
+ AddAccountDialog(newAccount, accountStateViewModel) { newAccount = null }
+ }
}
}
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt
index 35d8e089b..c19d788ff 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.navigation
+import android.net.Uri
import android.os.Bundle
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Immutable
@@ -39,6 +40,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size25dp
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
+import java.net.URLEncoder
@Immutable
sealed class Route(
@@ -223,10 +225,12 @@ sealed class Route(
object NewPost :
Route(
- route = "NewPost?baseReplyTo={baseReplyTo}"e={quote}&fork={fork}&version={version}&draft={draft}&enableMessageInterface={enableMessageInterface}",
+ route = "NewPost?message={message}&attachment={attachment}&baseReplyTo={baseReplyTo}"e={quote}&fork={fork}&version={version}&draft={draft}&enableMessageInterface={enableMessageInterface}",
icon = R.drawable.ic_moments,
arguments =
listOf(
+ navArgument("message") { type = NavType.StringType },
+ navArgument("attachment") { type = NavType.StringType },
navArgument("baseReplyTo") { type = NavType.StringType },
navArgument("quote") { type = NavType.StringType },
navArgument("fork") { type = NavType.StringType },
@@ -287,6 +291,8 @@ private fun getRouteWithArguments(
}
fun buildNewPostRoute(
+ draftMessage: String? = null,
+ attachment: Uri? = null,
baseReplyTo: String? = null,
quote: String? = null,
fork: String? = null,
@@ -295,6 +301,8 @@ fun buildNewPostRoute(
enableMessageInterface: Boolean = false,
): String =
"NewPost?" +
+ "message=${draftMessage?.let { URLEncoder.encode(it, "utf-8") } ?: ""}&" +
+ "attachment=${attachment?.let { URLEncoder.encode(it.toString(), "utf-8") } ?: ""}&" +
"baseReplyTo=${baseReplyTo ?: ""}&" +
"quote=${quote ?: ""}&" +
"fork=${fork ?: ""}&" +
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NewPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NewPostScreen.kt
index f9e3e0561..9554e2e25 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NewPostScreen.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NewPostScreen.kt
@@ -194,6 +194,8 @@ import java.lang.Math.round
@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class)
@Composable
fun NewPostScreen(
+ message: String? = null,
+ attachment: Uri? = null,
baseReplyTo: Note? = null,
quote: Note? = null,
fork: Note? = null,
@@ -231,6 +233,12 @@ fun NewPostScreen(
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
postViewModel.load(accountViewModel, baseReplyTo, quote, fork, version, draft)
+ message?.ifBlank { null }?.let {
+ postViewModel.updateMessage(TextFieldValue(it))
+ }
+ attachment?.let {
+ postViewModel.selectImage(it)
+ }
}
}
@@ -318,16 +326,16 @@ fun NewPostScreen(
) {
Column(
modifier =
- Modifier.fillMaxSize().padding(
- start = Size10dp,
- end = Size10dp,
- ),
+ Modifier.fillMaxSize(),
) {
Row(
modifier =
Modifier
.fillMaxWidth()
- .weight(1f),
+ .padding(
+ start = Size10dp,
+ end = Size10dp,
+ ).weight(1f),
) {
Column(
modifier =
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatroomScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatroomScreen.kt
index e6bdc1a45..37d8c1025 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatroomScreen.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatroomScreen.kt
@@ -387,7 +387,7 @@ fun PrepareChatroomViewModels(
}
if (draftMessage != null) {
- LaunchedEffect(key1 = draftMessage) { newPostModel.message = TextFieldValue(draftMessage) }
+ LaunchedEffect(key1 = draftMessage) { newPostModel.updateMessage(TextFieldValue(draftMessage)) }
}
ChatroomScreen(
diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/UrlDecoderTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/UrlDecoderTest.kt
new file mode 100644
index 000000000..2e7e9ebc0
--- /dev/null
+++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/UrlDecoderTest.kt
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2024 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.amethyst
+
+import junit.framework.TestCase.assertEquals
+import org.junit.Test
+import java.net.URLDecoder
+import java.net.URLEncoder
+
+class UrlDecoderTest {
+ val uri = "content://com.google.android.apps.photos.contentprovider/0/1/content%3A%2F%2Fmedia%2Fexternal%2Fimages%2Fmedia%2F1000023553/REQUIRE_ORIGINAL/NONE/image%2Fjpeg/913263593"
+
+ @Test
+ fun testRecursiveDecoding() {
+ val encoded = URLEncoder.encode(uri, "utf-8")
+ assertEquals("content%3A%2F%2Fcom.google.android.apps.photos.contentprovider%2F0%2F1%2Fcontent%253A%252F%252Fmedia%252Fexternal%252Fimages%252Fmedia%252F1000023553%2FREQUIRE_ORIGINAL%2FNONE%2Fimage%252Fjpeg%2F913263593", encoded)
+
+ val decoded = URLDecoder.decode(encoded, "utf-8")
+ assertEquals(uri, decoded)
+ }
+}