From 549b9f53e557e50a06f167b01432538bb02003b2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 16 Oct 2024 09:41:02 -0400 Subject: [PATCH 1/3] Fixes new media post dialog for the edge to edge borders --- .../amethyst/ui/actions/NewMediaView.kt | 133 +++++++++++------- .../ui/components/SetDialogToEdgeToEdge.kt | 49 +++++++ .../amethyst/ui/components/WindowUtils.kt | 54 +++++++ .../ui/components/ZoomableContentView.kt | 27 ---- 4 files changed, 182 insertions(+), 81 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SetDialogToEdgeToEdge.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/WindowUtils.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index ab3637138..15c63a565 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -30,8 +30,10 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding @@ -40,13 +42,17 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold import androidx.compose.material3.Slider import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -70,6 +76,7 @@ import androidx.compose.ui.window.DialogProperties import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.Nip96MediaServers +import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.ui.components.VideoView import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -80,6 +87,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size5dp +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.events.FileServersEvent import kotlinx.collections.immutable.toImmutableList @@ -87,6 +95,7 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +@OptIn(ExperimentalMaterial3Api::class) @Composable fun NewMediaView( uri: Uri, @@ -121,9 +130,66 @@ fun NewMediaView( decorFitsSystemWindows = false, ), ) { - Surface( - modifier = Modifier.fillMaxWidth(), - ) { + SetDialogToEdgeToEdge() + Scaffold( + topBar = { + TopAppBar( + title = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = StdHorzSpacer) + + Box { + IconButton( + modifier = Modifier.align(Alignment.Center), + onClick = { showRelaysDialog = true }, + ) { + Icon( + painter = painterResource(R.drawable.relays), + contentDescription = null, + modifier = Modifier.height(25.dp), + tint = MaterialTheme.colorScheme.onBackground, + ) + } + } + + PostButton( + onPost = { + onClose() + postViewModel.upload(context, relayList, mediaQualitySlider) { + accountViewModel.toast(stringRes(context, R.string.failed_to_upload_media_no_details), it) + } + postViewModel.selectedServer?.let { + if (!it.isNip95) { + account.settings.changeDefaultFileServer(it.server) + } + } + }, + isActive = postViewModel.canPost(), + ) + } + }, + navigationIcon = { + Row { + Spacer(modifier = StdHorzSpacer) + CloseButton( + onPress = { + postViewModel.cancel() + onClose() + }, + ) + } + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + ) + }, + ) { pad -> if (showRelaysDialog) { RelaySelectionDialog( preSelectedList = relayList, @@ -134,61 +200,21 @@ fun NewMediaView( ) } - Column( + Surface( modifier = Modifier - .padding(start = 10.dp, end = 10.dp, top = 10.dp) - .fillMaxWidth() - .fillMaxHeight() + .padding(pad) + .consumeWindowInsets(pad) .imePadding(), ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - CloseButton( - onPress = { - postViewModel.cancel() - onClose() - }, - ) - - Box { - IconButton( - modifier = Modifier.align(Alignment.Center), - onClick = { showRelaysDialog = true }, - ) { - Icon( - painter = painterResource(R.drawable.relays), - contentDescription = null, - modifier = Modifier.height(25.dp), - tint = MaterialTheme.colorScheme.onBackground, - ) - } - } - - PostButton( - onPost = { - onClose() - postViewModel.upload(context, relayList, mediaQualitySlider) { - accountViewModel.toast(stringRes(context, R.string.failed_to_upload_media_no_details), it) - } - postViewModel.selectedServer?.let { - if (!it.isNip95) { - account.settings.changeDefaultFileServer(it.server) - } - } - }, - isActive = postViewModel.canPost(), - ) - } - - Row( - modifier = Modifier.fillMaxWidth().weight(1f), + Column( + modifier = + Modifier + .fillMaxSize() + .padding(start = 10.dp, end = 10.dp, bottom = 10.dp), ) { Column( - modifier = Modifier.fillMaxWidth().verticalScroll(scrollState), + modifier = Modifier.fillMaxWidth().weight(1f).verticalScroll(scrollState), ) { ImageVideoPost(postViewModel, accountViewModel) @@ -197,7 +223,6 @@ fun NewMediaView( modifier = Modifier .fillMaxWidth() - .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)) .padding(vertical = 8.dp), ) { Column( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SetDialogToEdgeToEdge.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SetDialogToEdgeToEdge.kt new file mode 100644 index 000000000..9ce8e80b1 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SetDialogToEdgeToEdge.kt @@ -0,0 +1,49 @@ +/** + * 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.ui.components + +import android.view.View +import android.view.WindowManager +import android.widget.FrameLayout +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.window.DialogWindowProvider + +@Composable +fun SetDialogToEdgeToEdge() { + val activityWindow = getActivityWindow() + val dialogWindow = (LocalView.current.parent as? DialogWindowProvider)?.window + val parentView = LocalView.current.parent as View + SideEffect { + if (activityWindow != null && dialogWindow != null) { + val attributes = WindowManager.LayoutParams() + attributes.copyFrom(activityWindow.attributes) + attributes.type = dialogWindow.attributes.type + dialogWindow.attributes = attributes + parentView.layoutParams = + FrameLayout.LayoutParams( + activityWindow.decorView.width, + activityWindow.decorView.height, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/WindowUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/WindowUtils.kt new file mode 100644 index 000000000..15ea0e454 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/WindowUtils.kt @@ -0,0 +1,54 @@ +/** + * 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.ui.components + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.view.Window +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.window.DialogWindowProvider +import com.vitorpamplona.amethyst.ui.navigation.getActivity + +// Window utils +@Composable +fun getDialogWindow(): Window? = (LocalView.current.parent as? DialogWindowProvider)?.window + +@Composable +fun getActivityWindow(): Window? = LocalView.current.context.getActivityWindow() + +private tailrec fun Context.getActivityWindow(): Window? = + when (this) { + is Activity -> window + is ContextWrapper -> baseContext.getActivityWindow() + else -> null + } + +@Composable +fun getActivity(): Activity? = LocalView.current.context.getActivity() + +private tailrec fun Context.getActivity(): Activity? = + when (this) { + is Activity -> this + is ContextWrapper -> baseContext.getActivity() + else -> null + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 2911754e8..06726914d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -20,11 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.components -import android.app.Activity -import android.content.Context -import android.content.ContextWrapper import android.util.Log -import android.view.Window import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -62,7 +58,6 @@ import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle -import androidx.compose.ui.window.DialogWindowProvider import androidx.core.net.toUri import coil.annotation.ExperimentalCoilApi import coil.compose.AsyncImage @@ -820,25 +815,3 @@ private fun HashVerificationSymbol(verifiedHash: Boolean) { } } } - -// Window utils -@Composable -fun getDialogWindow(): Window? = (LocalView.current.parent as? DialogWindowProvider)?.window - -@Composable fun getActivityWindow(): Window? = LocalView.current.context.getActivityWindow() - -private tailrec fun Context.getActivityWindow(): Window? = - when (this) { - is Activity -> window - is ContextWrapper -> baseContext.getActivityWindow() - else -> null - } - -@Composable fun getActivity(): Activity? = LocalView.current.context.getActivity() - -private tailrec fun Context.getActivity(): Activity? = - when (this) { - is Activity -> this - is ContextWrapper -> baseContext.getActivity() - else -> null - } From 13e2546d362794f20f104a1c424f78850054da67 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 16 Oct 2024 10:54:57 -0400 Subject: [PATCH 2/3] Makes Amethyst a share target for texts, images and videos. --- amethyst/src/main/AndroidManifest.xml | 18 ++ .../amethyst/ui/navigation/AppNavigation.kt | 163 +++++++++++------- .../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, 176 insertions(+), 74 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 }, - ) - } - - 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) + } +} From 236ff2d680d62fbe74516cf832ce1567986f6aa4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 16 Oct 2024 11:13:56 -0400 Subject: [PATCH 3/3] v0.92.4 --- amethyst/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index 1d45abeff..893a8e8f6 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -16,8 +16,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk libs.versions.android.minSdk.get().toInteger() targetSdk libs.versions.android.targetSdk.get().toInteger() - versionCode 408 - versionName "0.92.3" + versionCode 409 + versionName "0.92.4" buildConfigField "String", "RELEASE_NOTES_ID", "\"98f2acaf9b3eb70d87607942e576f9a086ac28adf37869da9a29f4815cecac86\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"