Skip to content

Commit

Permalink
Makes Amethyst a share target for texts, images and videos.
Browse files Browse the repository at this point in the history
  • Loading branch information
vitorpamplona committed Oct 16, 2024
1 parent 549b9f5 commit 13e2546
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 69 deletions.
18 changes: 18 additions & 0 deletions amethyst/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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="" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -291,13 +293,20 @@ 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")
val version = it.arguments?.getString("version")
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) },
Expand Down Expand Up @@ -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 },
)
}
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<Parcelable>(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> { 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> { 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<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 }
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -223,10 +225,12 @@ sealed class Route(

object NewPost :
Route(
route = "NewPost?baseReplyTo={baseReplyTo}&quote={quote}&fork={fork}&version={version}&draft={draft}&enableMessageInterface={enableMessageInterface}",
route = "NewPost?message={message}&attachment={attachment}&baseReplyTo={baseReplyTo}&quote={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 },
Expand Down Expand Up @@ -287,6 +291,8 @@ private fun getRouteWithArguments(
}

fun buildNewPostRoute(
draftMessage: String? = null,
attachment: Uri? = null,
baseReplyTo: String? = null,
quote: String? = null,
fork: String? = null,
Expand All @@ -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 ?: ""}&" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
}
}

Expand Down Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ fun PrepareChatroomViewModels(
}

if (draftMessage != null) {
LaunchedEffect(key1 = draftMessage) { newPostModel.message = TextFieldValue(draftMessage) }
LaunchedEffect(key1 = draftMessage) { newPostModel.updateMessage(TextFieldValue(draftMessage)) }
}

ChatroomScreen(
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit 13e2546

Please sign in to comment.