From 13e2546d362794f20f104a1c424f78850054da67 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 16 Oct 2024 10:54:57 -0400 Subject: [PATCH] Makes Amethyst a share target for texts, images and videos. --- amethyst/src/main/AndroidManifest.xml | 18 +++ .../amethyst/ui/navigation/AppNavigation.kt | 153 +++++++++++------- .../amethyst/ui/navigation/Routes.kt | 10 +- .../ui/screen/loggedIn/NewPostScreen.kt | 18 ++- .../loggedIn/chatrooms/ChatroomScreen.kt | 2 +- .../vitorpamplona/amethyst/UrlDecoderTest.kt | 39 +++++ 6 files changed, 171 insertions(+), 69 deletions(-) create mode 100644 amethyst/src/test/java/com/vitorpamplona/amethyst/UrlDecoderTest.kt 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 }, - ) - } + if (activity.intent.action == Intent.ACTION_SEND) { + activity.intent.getStringExtra(Intent.EXTRA_TEXT)?.let { + nav.newStack(buildNewPostRoute(draftMessage = it)) + } - currentIntentNextPage?.let { intentNextPage -> - var actionableNextPage by remember { - mutableStateOf(uriToRoute(intentNextPage)) + (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 }, + ) + } + + currentIntentNextPage?.let { intentNextPage -> + var actionableNextPage by remember { + mutableStateOf(uriToRoute(intentNextPage)) + } - LaunchedEffect(intentNextPage) { - if (actionableNextPage != null) { - actionableNextPage?.let { - val currentRoute = getRouteWithArguments(nav.controller) - if (!isSameRoute(currentRoute, it)) { - nav.newStack(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 } + actionableNextPage = null - } - } else if (intentNextPage.contains("ncryptsec1")) { - // login functions - Nip19Bech32.tryParseAndClean(intentNextPage)?.let { - newAccount = it + } else { + accountViewModel.toast( + R.string.invalid_nip19_uri, + R.string.invalid_nip19_uri_description, + intentNextPage, + ) } - actionableNextPage = null - } else { - accountViewModel.toast( - R.string.invalid_nip19_uri, - R.string.invalid_nip19_uri_description, - intentNextPage, - ) + currentIntentNextPage = null } - - currentIntentNextPage = null } - } - val scope = rememberCoroutineScope() + 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) { - val currentRoute = getRouteWithArguments(nav.controller) - if (!isSameRoute(currentRoute, newPage)) { - nav.newStack(newPage) + DisposableEffect(nav, activity) { + val consumer = + Consumer { intent -> + if (intent.action == Intent.ACTION_SEND) { + intent.getStringExtra(Intent.EXTRA_TEXT)?.let { + nav.newStack(buildNewPostRoute(draftMessage = it)) } - } else if (uri.contains("ncryptsec")) { - // login functions - Nip19Bech32.tryParseAndClean(uri)?.let { - newAccount = 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) + } +}