Merge remote-tracking branch 'origin/profiles-list-management' into profiles-list-management

This commit is contained in:
KotlinGeekDev
2024-10-16 16:28:01 +01:00
11 changed files with 360 additions and 157 deletions
+2 -2
View File
@@ -16,8 +16,8 @@ android {
applicationId "com.vitorpamplona.amethyst" applicationId "com.vitorpamplona.amethyst"
minSdk libs.versions.android.minSdk.get().toInteger() minSdk libs.versions.android.minSdk.get().toInteger()
targetSdk libs.versions.android.targetSdk.get().toInteger() targetSdk libs.versions.android.targetSdk.get().toInteger()
versionCode 408 versionCode 409
versionName "0.92.3" versionName "0.92.4"
buildConfigField "String", "RELEASE_NOTES_ID", "\"98f2acaf9b3eb70d87607942e576f9a086ac28adf37869da9a29f4815cecac86\"" buildConfigField "String", "RELEASE_NOTES_ID", "\"98f2acaf9b3eb70d87607942e576f9a086ac28adf37869da9a29f4815cecac86\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+18
View File
@@ -103,6 +103,24 @@
<data android:scheme="nostr+walletconnect" /> <data android:scheme="nostr+walletconnect" />
</intent-filter> </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 <meta-data
android:name="android.app.lib_name" android:name="android.app.lib_name"
android:value="" /> android:value="" />
@@ -30,8 +30,10 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets 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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding 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.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -70,6 +76,7 @@ import androidx.compose.ui.window.DialogProperties
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.Nip96MediaServers 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.components.VideoView
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel 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.screen.loggedIn.TitleExplainer
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.FileServersEvent import com.vitorpamplona.quartz.events.FileServersEvent
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -87,6 +95,7 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun NewMediaView( fun NewMediaView(
uri: Uri, uri: Uri,
@@ -121,38 +130,17 @@ fun NewMediaView(
decorFitsSystemWindows = false, decorFitsSystemWindows = false,
), ),
) { ) {
Surface( SetDialogToEdgeToEdge()
modifier = Modifier.fillMaxWidth(), Scaffold(
) { topBar = {
if (showRelaysDialog) { TopAppBar(
RelaySelectionDialog( title = {
preSelectedList = relayList,
onClose = { showRelaysDialog = false },
onPost = { relayList = it },
accountViewModel = accountViewModel,
nav = nav,
)
}
Column(
modifier =
Modifier
.padding(start = 10.dp, end = 10.dp, top = 10.dp)
.fillMaxWidth()
.fillMaxHeight()
.imePadding(),
) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
CloseButton( Spacer(modifier = StdHorzSpacer)
onPress = {
postViewModel.cancel()
onClose()
},
)
Box { Box {
IconButton( IconButton(
@@ -183,12 +171,50 @@ fun NewMediaView(
isActive = postViewModel.canPost(), 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,
onClose = { showRelaysDialog = false },
onPost = { relayList = it },
accountViewModel = accountViewModel,
nav = nav,
)
}
Row( Surface(
modifier = Modifier.fillMaxWidth().weight(1f), modifier =
Modifier
.padding(pad)
.consumeWindowInsets(pad)
.imePadding(),
) { ) {
Column( Column(
modifier = Modifier.fillMaxWidth().verticalScroll(scrollState), modifier =
Modifier
.fillMaxSize()
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
) {
Column(
modifier = Modifier.fillMaxWidth().weight(1f).verticalScroll(scrollState),
) { ) {
ImageVideoPost(postViewModel, accountViewModel) ImageVideoPost(postViewModel, accountViewModel)
@@ -197,7 +223,6 @@ fun NewMediaView(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
.padding(vertical = 8.dp), .padding(vertical = 8.dp),
) { ) {
Column( Column(
@@ -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,
)
}
}
}
@@ -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
}
@@ -20,11 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.ui.components package com.vitorpamplona.amethyst.ui.components
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.util.Log import android.util.Log
import android.view.Window
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut 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.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.window.DialogWindowProvider
import androidx.core.net.toUri import androidx.core.net.toUri
import coil.annotation.ExperimentalCoilApi import coil.annotation.ExperimentalCoilApi
import coil.compose.AsyncImage 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
}
@@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.ui.navigation
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Parcelable
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@@ -291,6 +293,11 @@ fun AppNavigation(
popEnterTransition = { scaleIn }, popEnterTransition = { scaleIn },
popExitTransition = { slideOutVerticallyToBottom }, 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 baseReplyTo = it.arguments?.getString("baseReplyTo")
val quote = it.arguments?.getString("quote") val quote = it.arguments?.getString("quote")
val fork = it.arguments?.getString("fork") val fork = it.arguments?.getString("fork")
@@ -298,6 +305,8 @@ fun AppNavigation(
val draft = it.arguments?.getString("draft") val draft = it.arguments?.getString("draft")
val enableMessageInterface = it.arguments?.getBoolean("enableMessageInterface") ?: false val enableMessageInterface = it.arguments?.getBoolean("enableMessageInterface") ?: false
NewPostScreen( NewPostScreen(
message = draftMessage,
attachment = attachment,
baseReplyTo = baseReplyTo?.let { hex -> accountViewModel.getNoteIfExists(hex) }, baseReplyTo = baseReplyTo?.let { hex -> accountViewModel.getNoteIfExists(hex) },
quote = quote?.let { hex -> accountViewModel.getNoteIfExists(hex) }, quote = quote?.let { hex -> accountViewModel.getNoteIfExists(hex) },
fork = fork?.let { hex -> accountViewModel.getNoteIfExists(hex) }, fork = fork?.let { hex -> accountViewModel.getNoteIfExists(hex) },
@@ -326,6 +335,15 @@ private fun NavigateIfIntentRequested(
val activity = LocalContext.current.getActivity() val activity = LocalContext.current.getActivity()
var newAccount by remember { mutableStateOf<String?>(null) } var newAccount by remember { mutableStateOf<String?>(null) }
if (activity.intent.action == Intent.ACTION_SEND) {
activity.intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
nav.newStack(buildNewPostRoute(draftMessage = it))
}
(activity.intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
nav.newStack(buildNewPostRoute(attachment = it))
}
} else {
var currentIntentNextPage by remember { var currentIntentNextPage by remember {
mutableStateOf( mutableStateOf(
activity.intent activity.intent
@@ -373,6 +391,15 @@ private fun NavigateIfIntentRequested(
DisposableEffect(nav, activity) { DisposableEffect(nav, activity) {
val consumer = val consumer =
Consumer<Intent> { intent -> 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 {
val uri = intent.data?.toString() val uri = intent.data?.toString()
if (!uri.isNullOrBlank()) { if (!uri.isNullOrBlank()) {
// navigation functions // navigation functions
@@ -400,6 +427,7 @@ private fun NavigateIfIntentRequested(
} }
} }
} }
}
activity.addOnNewIntentListener(consumer) activity.addOnNewIntentListener(consumer)
onDispose { activity.removeOnNewIntentListener(consumer) } onDispose { activity.removeOnNewIntentListener(consumer) }
} }
@@ -407,6 +435,7 @@ private fun NavigateIfIntentRequested(
if (newAccount != null) { if (newAccount != null) {
AddAccountDialog(newAccount, accountStateViewModel) { newAccount = null } AddAccountDialog(newAccount, accountStateViewModel) { newAccount = null }
} }
}
} }
fun Context.getActivity(): MainActivity { fun Context.getActivity(): MainActivity {
@@ -20,6 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.ui.navigation package com.vitorpamplona.amethyst.ui.navigation
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Immutable 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.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import java.net.URLEncoder
@Immutable @Immutable
sealed class Route( sealed class Route(
@@ -230,10 +232,12 @@ sealed class Route(
object NewPost : object NewPost :
Route( 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, icon = R.drawable.ic_moments,
arguments = arguments =
listOf( listOf(
navArgument("message") { type = NavType.StringType },
navArgument("attachment") { type = NavType.StringType },
navArgument("baseReplyTo") { type = NavType.StringType }, navArgument("baseReplyTo") { type = NavType.StringType },
navArgument("quote") { type = NavType.StringType }, navArgument("quote") { type = NavType.StringType },
navArgument("fork") { type = NavType.StringType }, navArgument("fork") { type = NavType.StringType },
@@ -294,6 +298,8 @@ private fun getRouteWithArguments(
} }
fun buildNewPostRoute( fun buildNewPostRoute(
draftMessage: String? = null,
attachment: Uri? = null,
baseReplyTo: String? = null, baseReplyTo: String? = null,
quote: String? = null, quote: String? = null,
fork: String? = null, fork: String? = null,
@@ -302,6 +308,8 @@ fun buildNewPostRoute(
enableMessageInterface: Boolean = false, enableMessageInterface: Boolean = false,
): String = ): String =
"NewPost?" + "NewPost?" +
"message=${draftMessage?.let { URLEncoder.encode(it, "utf-8") } ?: ""}&" +
"attachment=${attachment?.let { URLEncoder.encode(it.toString(), "utf-8") } ?: ""}&" +
"baseReplyTo=${baseReplyTo ?: ""}&" + "baseReplyTo=${baseReplyTo ?: ""}&" +
"quote=${quote ?: ""}&" + "quote=${quote ?: ""}&" +
"fork=${fork ?: ""}&" + "fork=${fork ?: ""}&" +
@@ -194,6 +194,8 @@ import java.lang.Math.round
@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class) @OptIn(ExperimentalMaterial3Api::class, FlowPreview::class)
@Composable @Composable
fun NewPostScreen( fun NewPostScreen(
message: String? = null,
attachment: Uri? = null,
baseReplyTo: Note? = null, baseReplyTo: Note? = null,
quote: Note? = null, quote: Note? = null,
fork: Note? = null, fork: Note? = null,
@@ -231,6 +233,12 @@ fun NewPostScreen(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
postViewModel.load(accountViewModel, baseReplyTo, quote, fork, version, draft) 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( Column(
modifier = modifier =
Modifier.fillMaxSize().padding( Modifier.fillMaxSize(),
start = Size10dp,
end = Size10dp,
),
) { ) {
Row( Row(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f), .padding(
start = Size10dp,
end = Size10dp,
).weight(1f),
) { ) {
Column( Column(
modifier = modifier =
@@ -387,7 +387,7 @@ fun PrepareChatroomViewModels(
} }
if (draftMessage != null) { if (draftMessage != null) {
LaunchedEffect(key1 = draftMessage) { newPostModel.message = TextFieldValue(draftMessage) } LaunchedEffect(key1 = draftMessage) { newPostModel.updateMessage(TextFieldValue(draftMessage)) }
} }
ChatroomScreen( 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)
}
}