mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-09-27 20:56:15 +02:00
Makes Amethyst a share target for texts, images and videos.
This commit is contained in:
@@ -103,6 +103,24 @@
|
||||
<data android:scheme="nostr+walletconnect" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="New Post">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="New Post">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="New Post">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.lib_name"
|
||||
android:value="" />
|
||||
|
@@ -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<String?>(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<Parcelable>(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> { 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> { intent ->
|
||||
if (intent.action == Intent.ACTION_SEND) {
|
||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||
nav.newStack(buildNewPostRoute(draftMessage = it))
|
||||
}
|
||||
|
||||
(intent.getParcelableExtra<Parcelable>(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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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 ?: ""}&" +
|
||||
|
@@ -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 =
|
||||
|
@@ -387,7 +387,7 @@ fun PrepareChatroomViewModels(
|
||||
}
|
||||
|
||||
if (draftMessage != null) {
|
||||
LaunchedEffect(key1 = draftMessage) { newPostModel.message = TextFieldValue(draftMessage) }
|
||||
LaunchedEffect(key1 = draftMessage) { newPostModel.updateMessage(TextFieldValue(draftMessage)) }
|
||||
}
|
||||
|
||||
ChatroomScreen(
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user