mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-06 22:54:39 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b11d0ce81 | ||
|
|
26ba75c7c9 | ||
|
|
67fb3bf488 | ||
|
|
24b29396cb | ||
|
|
25c3532637 | ||
|
|
cff6ee457d | ||
|
|
c0ba6f5b00 | ||
|
|
44d8b8ff33 | ||
|
|
f495acb570 | ||
|
|
ecfc8bcd87 | ||
|
|
724bea6bbb | ||
|
|
6826db13a4 | ||
|
|
c5dd2c5651 | ||
|
|
236ff2d680 | ||
|
|
13e2546d36 | ||
|
|
549b9f53e5 | ||
|
|
974c022aed | ||
|
|
a57566dd84 | ||
|
|
72e86cf391 | ||
|
|
3b23cded56 | ||
|
|
121337eb83 | ||
|
|
fe87d642c9 | ||
|
|
b8112ff1aa | ||
|
|
1eee0bd1ae | ||
|
|
f48335e3be | ||
|
|
fcc4befb22 | ||
|
|
e28a1896ec | ||
|
|
f10e66e6a5 | ||
|
|
825a227e2d | ||
|
|
3446fdaf7f | ||
|
|
9c29bf3f25 | ||
|
|
9642e45775 | ||
|
|
58788d2cd5 | ||
|
|
d7c18341cd | ||
|
|
ee16f6c143 | ||
|
|
d78b55d134 | ||
|
|
ff5e7359e2 | ||
|
|
4fd0f38402 | ||
|
|
5b516f4101 | ||
|
|
1fc390c215 | ||
|
|
2bb09f583b | ||
|
|
89de272ea1 |
+4
@@ -37,6 +37,10 @@
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
|
||||
@@ -16,8 +16,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk libs.versions.android.minSdk.get().toInteger()
|
||||
targetSdk libs.versions.android.targetSdk.get().toInteger()
|
||||
versionCode 407
|
||||
versionName "0.92.2"
|
||||
versionCode 411
|
||||
versionName "0.92.6"
|
||||
buildConfigField "String", "RELEASE_NOTES_ID", "\"98f2acaf9b3eb70d87607942e576f9a086ac28adf37869da9a29f4815cecac86\""
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -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="" />
|
||||
|
||||
@@ -140,7 +140,10 @@ class Amethyst : Application() {
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
println("Trim Memory $level")
|
||||
GlobalScope.launch(Dispatchers.Default) { serviceManager.trimMemory() }
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
println("Trim Memory Inside $level")
|
||||
serviceManager.trimMemory()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -357,7 +357,7 @@ class Account(
|
||||
mappedRelaySet.map { relay ->
|
||||
val nip65setup = nip65RelaySet?.firstOrNull { relay.url == it.relayUrl }
|
||||
if (nip65setup != null) {
|
||||
val write = nip65setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.BOTH || nip65setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.READ
|
||||
val write = nip65setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.BOTH || nip65setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.WRITE
|
||||
|
||||
RelaySetupInfo(
|
||||
relay.url,
|
||||
@@ -377,7 +377,7 @@ class Account(
|
||||
|
||||
nip65RelaySet?.forEach { newNip65Setup ->
|
||||
if (mappedRelaySet.none { it.url == newNip65Setup.relayUrl }) {
|
||||
val write = newNip65Setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.BOTH || newNip65Setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.READ
|
||||
val write = newNip65Setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.BOTH || newNip65Setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.WRITE
|
||||
|
||||
mappedRelaySet = mappedRelaySet +
|
||||
RelaySetupInfo(
|
||||
|
||||
@@ -97,6 +97,9 @@ import com.vitorpamplona.amethyst.ui.components.VideoView
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ImageVideoDescription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.UserLine
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
|
||||
@@ -80,6 +80,7 @@ import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.SearchIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChannelName
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchBarViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
@@ -47,6 +47,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
|
||||
@@ -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,13 +76,18 @@ 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
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SettingSwitchItem
|
||||
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
|
||||
@@ -84,6 +95,7 @@ import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NewMediaView(
|
||||
uri: Uri,
|
||||
@@ -118,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,
|
||||
@@ -131,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)
|
||||
|
||||
@@ -194,7 +223,6 @@ fun NewMediaView(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
||||
.padding(vertical = 8.dp),
|
||||
) {
|
||||
Column(
|
||||
|
||||
@@ -981,6 +981,10 @@ open class NewPostViewModel : ViewModel() {
|
||||
draftTextChanges.trySend("")
|
||||
}
|
||||
|
||||
open fun addToMessage(it: String) {
|
||||
updateMessage(TextFieldValue(message.text + " " + it))
|
||||
}
|
||||
|
||||
open fun updateMessage(it: TextFieldValue) {
|
||||
message = it
|
||||
urlPreview = findUrlInMessage()
|
||||
|
||||
@@ -46,6 +46,8 @@ import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
|
||||
@@ -50,6 +50,8 @@ import com.vitorpamplona.amethyst.service.Nip11Retriever
|
||||
import com.vitorpamplona.amethyst.ui.actions.relays.RelayInformationDialog
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
|
||||
|
||||
+2
-2
@@ -55,11 +55,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.relays.SettingsCategoryWithButton
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
|
||||
+2
-2
@@ -42,10 +42,10 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
|
||||
+2
-2
@@ -42,10 +42,10 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
|
||||
+2
-2
@@ -49,11 +49,11 @@ import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.MinHorzSpacer
|
||||
|
||||
+1
-1
@@ -50,7 +50,6 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableEmail
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
||||
@@ -61,6 +60,7 @@ import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.UserCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
|
||||
+49
@@ -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
|
||||
}
|
||||
-27
@@ -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
|
||||
}
|
||||
|
||||
@@ -23,13 +23,17 @@ 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
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -52,6 +56,7 @@ import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRedirectScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NewPostScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.BookmarkListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChannelScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChatroomListScreen
|
||||
@@ -270,15 +275,49 @@ fun AppNavigation(
|
||||
composable(
|
||||
Route.NIP47Setup.route,
|
||||
Route.NIP47Setup.arguments,
|
||||
enterTransition = { slideInHorizontallyFromEnd },
|
||||
enterTransition = { slideInVerticallyFromBottom },
|
||||
exitTransition = { scaleOut },
|
||||
popEnterTransition = { scaleIn },
|
||||
popExitTransition = { slideOutHorizontallyToEnd },
|
||||
popExitTransition = { slideOutVerticallyToBottom },
|
||||
) {
|
||||
val nip47 = it.arguments?.getString("nip47")
|
||||
|
||||
NIP47SetupScreen(accountViewModel, nav, nip47)
|
||||
}
|
||||
|
||||
composable(
|
||||
Route.NewPost.route,
|
||||
Route.NewPost.arguments,
|
||||
enterTransition = { slideInVerticallyFromBottom },
|
||||
exitTransition = { scaleOut },
|
||||
popEnterTransition = { scaleIn },
|
||||
popExitTransition = { slideOutVerticallyToBottom },
|
||||
) {
|
||||
val draftMessage = it.arguments?.getString("message")?.ifBlank { null }
|
||||
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) },
|
||||
version = version?.let { hex -> accountViewModel.getNoteIfExists(hex) },
|
||||
draft = draft?.let { hex -> accountViewModel.getNoteIfExists(hex) },
|
||||
enableMessageInterface = enableMessageInterface,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,88 +334,129 @@ private fun NavigateIfIntentRequested(
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
) {
|
||||
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) {
|
||||
// avoids restarting the new Post screen when the intent is for the screen.
|
||||
// Microsoft's swift key sends Gifs as new actions
|
||||
if (isBaseRoute(nav.controller, Route.NewPost.base)) return
|
||||
|
||||
currentIntentNextPage?.let { intentNextPage ->
|
||||
var actionableNextPage by remember {
|
||||
mutableStateOf(uriToRoute(intentNextPage))
|
||||
// saves the intent to avoid processing again
|
||||
var message by remember {
|
||||
mutableStateOf(
|
||||
activity.intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||
it.ifBlank { null }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
var media by remember {
|
||||
mutableStateOf(
|
||||
(activity.intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri),
|
||||
)
|
||||
}
|
||||
|
||||
actionableNextPage = null
|
||||
} else {
|
||||
accountViewModel.toast(
|
||||
R.string.invalid_nip19_uri,
|
||||
R.string.invalid_nip19_uri_description,
|
||||
intentNextPage,
|
||||
)
|
||||
nav.newStack(buildNewPostRoute(draftMessage = message, attachment = media))
|
||||
|
||||
media = null
|
||||
message = null
|
||||
} else {
|
||||
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))
|
||||
}
|
||||
|
||||
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) {
|
||||
// avoids restarting the new Post screen when the intent is for the screen.
|
||||
// Microsoft's swift key sends Gifs as new actions
|
||||
if (!isBaseRoute(nav.controller, Route.NewPost.base)) {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,6 +484,9 @@ private fun isSameRoute(
|
||||
return false
|
||||
}
|
||||
|
||||
val slideInVerticallyFromBottom = slideInVertically(animationSpec = tween(), initialOffsetY = { it })
|
||||
val slideOutVerticallyToBottom = slideOutVertically(animationSpec = tween(), targetOffsetY = { it })
|
||||
|
||||
val slideInHorizontallyFromEnd = slideInHorizontally(animationSpec = tween(), initialOffsetX = { it })
|
||||
val slideOutHorizontallyToEnd = slideOutHorizontally(animationSpec = tween(), targetOffsetX = { it })
|
||||
|
||||
|
||||
@@ -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(
|
||||
@@ -220,8 +222,34 @@ sealed class Route(
|
||||
},
|
||||
).toImmutableList(),
|
||||
)
|
||||
|
||||
object NewPost :
|
||||
Route(
|
||||
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 },
|
||||
navArgument("version") { type = NavType.StringType },
|
||||
navArgument("draft") { type = NavType.StringType },
|
||||
navArgument("enableMessageInterface") { type = NavType.BoolType },
|
||||
).toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
fun isBaseRoute(
|
||||
navController: NavHostController,
|
||||
startsWith: String,
|
||||
): Boolean =
|
||||
navController.currentBackStackEntry
|
||||
?.destination
|
||||
?.route
|
||||
?.startsWith(startsWith) ?: false
|
||||
|
||||
fun getRouteWithArguments(navController: NavHostController): String? {
|
||||
val currentEntry = navController.currentBackStackEntry ?: return null
|
||||
return getRouteWithArguments(currentEntry.destination, currentEntry.arguments)
|
||||
@@ -270,3 +298,23 @@ private fun getRouteWithArguments(
|
||||
}
|
||||
return route
|
||||
}
|
||||
|
||||
fun buildNewPostRoute(
|
||||
draftMessage: String? = null,
|
||||
attachment: Uri? = null,
|
||||
baseReplyTo: String? = null,
|
||||
quote: String? = null,
|
||||
fork: String? = null,
|
||||
version: String? = null,
|
||||
draft: String? = null,
|
||||
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 ?: ""}&" +
|
||||
"version=${version ?: ""}&" +
|
||||
"draft=${draft ?: ""}&" +
|
||||
"enableMessageInterface=$enableMessageInterface"
|
||||
|
||||
@@ -85,10 +85,10 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.components.SelectTextDialog
|
||||
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.buildNewPostRoute
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
@@ -201,24 +201,16 @@ fun NoteQuickActionMenu(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val editDraftDialog = remember { mutableStateOf(false) }
|
||||
|
||||
if (editDraftDialog.value) {
|
||||
NewPostView(
|
||||
onClose = {
|
||||
onDismiss()
|
||||
editDraftDialog.value = false
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
draft = note,
|
||||
nav = EmptyNav,
|
||||
)
|
||||
}
|
||||
|
||||
NoteQuickActionMenu(
|
||||
note = note,
|
||||
onDismiss = onDismiss,
|
||||
onWantsToEditDraft = { editDraftDialog.value = true },
|
||||
onWantsToEditDraft = {
|
||||
val route =
|
||||
buildNewPostRoute(
|
||||
draft = note.idHex,
|
||||
)
|
||||
nav.nav(route)
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
|
||||
@@ -59,41 +59,177 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.StringToastMsg
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockVitorAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BigPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.ripple24dp
|
||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PollNotePreview() {
|
||||
val event =
|
||||
PollNoteEvent(
|
||||
id = "6ff9bc13d27490f6e3953325260bd996901a143de89886a0608c39e7d0160a72",
|
||||
pubKey = "f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a",
|
||||
createdAt = 1729186078,
|
||||
content = "Testing polls again",
|
||||
sig = "540101837a8826e2ae28401ee5f4fd8606def8501bec92a74a9e05264bb2c67558b927edf23bb085f5b5f0d91a61c65a25c37a3b92075bf10c9be03dbbe8e94e",
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("poll_option", "0", "OP1"),
|
||||
arrayOf("poll_option", "1", "OP2"),
|
||||
arrayOf("poll_option", "2", "OP3"),
|
||||
arrayOf("value_maximum", "2"),
|
||||
arrayOf("value_minimum", "2"),
|
||||
arrayOf("alt", "Poll event"),
|
||||
),
|
||||
)
|
||||
|
||||
val zapVote =
|
||||
LnZapEvent(
|
||||
id = "2a17fdcd0e387d1623c7313d7aa2848e18dde8a942cfe8a2d6b686ea5f68f01a",
|
||||
pubKey = "79f00d3f5a19ec806189fcab03c1be4ff81d18ee4f653c88fac41fe03570f432",
|
||||
createdAt = 1729186293,
|
||||
content = "Testing polls again",
|
||||
sig = "819cbd8daccd173bc411f71deb4dc0fd7d281797f7f0318d2fbc6c5076e4ebe0aa52c16fd113ca5fcc3ee133cd0c4a11c6d3f77e4fa957d5401da1aff07028d0",
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("p", "f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a"),
|
||||
arrayOf("e", "6ff9bc13d27490f6e3953325260bd996901a143de89886a0608c39e7d0160a72"),
|
||||
arrayOf("P", "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
arrayOf("bolt11", "lnbc20n1pn3zj05dqqnp4qtfc238rdkzsj26waa3l8zgag9damzltzsqcrlscj9gvpc7ch2qs2pp5nz0dn8hg5d8z5z9cgd3x8uhnv68hkgg03v3phqvdm26r0trcm27ssp5qxgfplfnfl9q6lh96stly8t90zgkyy4emlgdhadgp8n60eh8ghzs9qyysgqcqpcxqyz5vqrzjqvdnqyc82a9maxu6c7mee0shqr33u4z9z04wpdwhf96gxzpln8jcrapyqqqqqqp2rcqqqqlgqqqqqzsq2qrzjqw9fu4j39mycmg440ztkraa03u5qhtuc5zfgydsv6ml38qd4azymlapyqqqqqqqp9sqqqqlgqqqq86qqjqrzjq26922n6s5n5undqrf78rjjhgpcczafws45tx8237y7pzx3fg8wwxrgayyqq2mgqqqqqqqqqqqqqqqqq2qzkp9q0nyx5508kumhsa8c5x82c96nuccvszlmann6mzf7qvjagwsmmft3acvvqz5q92uf5et4yr53zcgmvphtg5xephcpe5lqle26kcp530mr3"),
|
||||
arrayOf("preimage", "a2ea9951a3ced7bc9d6099ce28a12f027bb1c2116106a6baf7001122c3e08891"),
|
||||
arrayOf("description", "{\"id\":\"c37b692b2bb23cd543b7643962d336fe3ae96a5330769ea047808b20f80b0c6f\",\"pubkey\":\"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c\",\"created_at\":1729186292,\"kind\":9734,\"tags\":[[\"e\",\"6ff9bc13d27490f6e3953325260bd996901a143de89886a0608c39e7d0160a72\"],[\"p\",\"f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a\"],[\"relays\",\"wss://nos.lol/\",\"wss://vitor.nostr1.com/\"],[\"alt\",\"Zap request\"],[\"poll_option\",\"0\"]],\"content\":\"\",\"sig\":\"83f2243e27d804bad13443937ddf71de7c049e85f71264eaf3a1b751a04844a65ec4bb5916722a1cdf9248f46dc5e03a92be9de543f03d022fb3e7ea08d1cf00\"}"),
|
||||
),
|
||||
)
|
||||
|
||||
val accountViewModel = mockVitorAccountViewModel()
|
||||
val nav = EmptyNav
|
||||
val baseNote: Note?
|
||||
|
||||
runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
LocalCache.justConsume(event, null)
|
||||
LocalCache.consume(zapVote.zapRequest!!)
|
||||
LocalCache.consume(zapVote, null)
|
||||
baseNote = LocalCache.getOrCreateNote("6ff9bc13d27490f6e3953325260bd996901a143de89886a0608c39e7d0160a72")
|
||||
}
|
||||
}
|
||||
|
||||
val color = MaterialTheme.colorScheme.background
|
||||
|
||||
if (baseNote != null) {
|
||||
ThemeComparisonColumn(
|
||||
toPreview = {
|
||||
Column(
|
||||
Modifier.padding(10.dp),
|
||||
) {
|
||||
PollNote(
|
||||
baseNote = baseNote,
|
||||
true,
|
||||
remember { mutableStateOf(color) },
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PollNotePreview2() {
|
||||
val event =
|
||||
PollNoteEvent(
|
||||
id = "3064bf97800a4b04b612fc0fd498936eae75fffbdca5bbd09d19a6dc598530ab",
|
||||
pubKey = "f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a",
|
||||
createdAt = 1729191389,
|
||||
content = "Test",
|
||||
sig = "fc7f83e4dc84082a9a473338d9df2fdb876c59fbc94d49fdbce1e0bfd85cc7d50b14d4f0d08e8a84ef46f99a4e889e261cc898e2caeca4a153d8925254c5a761",
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("poll_option", "0", "Pesquisa em portugues"),
|
||||
arrayOf("poll_option", "1", "Pesquisa em ingles"),
|
||||
arrayOf("value_maximum", "2"),
|
||||
arrayOf("value_minimum", "2"),
|
||||
arrayOf("alt", "Poll event"),
|
||||
),
|
||||
)
|
||||
|
||||
val accountViewModel = mockAccountViewModel()
|
||||
val nav = EmptyNav
|
||||
val baseNote: Note?
|
||||
|
||||
runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
LocalCache.justConsume(event, null)
|
||||
baseNote = LocalCache.getOrCreateNote("3064bf97800a4b04b612fc0fd498936eae75fffbdca5bbd09d19a6dc598530ab")
|
||||
}
|
||||
}
|
||||
|
||||
val color = MaterialTheme.colorScheme.background
|
||||
|
||||
if (baseNote != null) {
|
||||
ThemeComparisonColumn(
|
||||
toPreview = {
|
||||
Column(
|
||||
Modifier.padding(10.dp),
|
||||
) {
|
||||
PollNote(
|
||||
baseNote = baseNote,
|
||||
true,
|
||||
remember { mutableStateOf(color) },
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PollNote(
|
||||
baseNote: Note,
|
||||
@@ -202,6 +338,7 @@ private fun OptionNote(
|
||||
nonClickablePrepend = {},
|
||||
clickablePrepend = {
|
||||
RenderOptionBeforeVote(
|
||||
baseNote,
|
||||
poolOption.descriptor,
|
||||
canPreview,
|
||||
tags,
|
||||
@@ -241,6 +378,9 @@ private fun RenderOptionAfterVote(
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.matchParentSize(),
|
||||
color = color,
|
||||
gapSize = 0.dp,
|
||||
strokeCap = StrokeCap.Square,
|
||||
drawStopIndicator = {},
|
||||
progress = {
|
||||
poolOption.tally.value.toFloat()
|
||||
},
|
||||
@@ -291,6 +431,7 @@ private fun RenderOptionAfterVote(
|
||||
|
||||
@Composable
|
||||
private fun RenderOptionBeforeVote(
|
||||
baseNote: Note,
|
||||
description: String,
|
||||
canPreview: Boolean,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
@@ -308,17 +449,19 @@ private fun RenderOptionBeforeVote(
|
||||
QuoteBorder,
|
||||
),
|
||||
) {
|
||||
TranslatableRichTextViewer(
|
||||
content = description,
|
||||
canPreview = canPreview,
|
||||
quotesLeft = 1,
|
||||
modifier = BigPadding,
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
id = description,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
Column(BigPadding) {
|
||||
TranslatableRichTextViewer(
|
||||
content = description,
|
||||
canPreview = canPreview,
|
||||
quotesLeft = 1,
|
||||
modifier = Modifier,
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
id = baseNote.idHex + description,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ContentTransform
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
@@ -46,7 +48,6 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -79,6 +80,7 @@ import androidx.compose.ui.Alignment.Companion.CenterStart
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.semantics.Role
|
||||
@@ -103,20 +105,22 @@ import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.buildNewPostRoute
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.types.EditState
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderReaction
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DarkerGreen
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Height24dpFilledModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Height4dpFilledModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.NoSoTinyBorders
|
||||
import com.vitorpamplona.amethyst.ui.theme.ReactionRowExpandButton
|
||||
@@ -135,7 +139,10 @@ import com.vitorpamplona.amethyst.ui.theme.Size28Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.TinyBorders
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.defaultTweenDuration
|
||||
import com.vitorpamplona.amethyst.ui.theme.defaultTweenFloatSpec
|
||||
import com.vitorpamplona.amethyst.ui.theme.defaultTweenIntOffsetSpec
|
||||
import com.vitorpamplona.amethyst.ui.theme.fundraiserProgressColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.reactionBox
|
||||
import com.vitorpamplona.amethyst.ui.theme.ripple24dp
|
||||
@@ -342,22 +349,13 @@ fun RenderZapRaiser(
|
||||
}
|
||||
}
|
||||
|
||||
val color =
|
||||
if (zapraiserStatus.progress > 0.99) {
|
||||
DarkerGreen
|
||||
} else {
|
||||
MaterialTheme.colorScheme.mediumImportanceLink
|
||||
}
|
||||
|
||||
LinearProgressIndicator(
|
||||
modifier =
|
||||
remember(details) {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(if (details) 24.dp else 4.dp)
|
||||
},
|
||||
color = color,
|
||||
modifier = if (details) Height24dpFilledModifier else Height4dpFilledModifier,
|
||||
color = MaterialTheme.colorScheme.fundraiserProgressColor,
|
||||
progress = { zapraiserStatus.progress },
|
||||
gapSize = 0.dp,
|
||||
strokeCap = StrokeCap.Square,
|
||||
drawStopIndicator = {},
|
||||
)
|
||||
|
||||
if (details) {
|
||||
@@ -374,7 +372,7 @@ fun RenderZapRaiser(
|
||||
text =
|
||||
stringRes(id = R.string.sats_to_complete, totalPercentage, zapraiserStatus.left),
|
||||
modifier = NoSoTinyBorders,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
// color = MaterialTheme.colorScheme.placeholderText,
|
||||
fontSize = Font14SP,
|
||||
maxLines = 1,
|
||||
)
|
||||
@@ -563,14 +561,17 @@ private fun BoostWithDialog(
|
||||
var wantsToFork by remember { mutableStateOf<Note?>(null) }
|
||||
|
||||
if (wantsToQuote != null) {
|
||||
NewPostView(
|
||||
onClose = { wantsToQuote = null },
|
||||
baseReplyTo = null,
|
||||
quote = wantsToQuote,
|
||||
version = (editState.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
val route =
|
||||
buildNewPostRoute(
|
||||
quote = wantsToQuote?.idHex,
|
||||
version =
|
||||
(editState.value as? GenericLoadable.Loaded)
|
||||
?.loaded
|
||||
?.modificationToShow
|
||||
?.value
|
||||
?.idHex,
|
||||
)
|
||||
nav.nav(route)
|
||||
}
|
||||
|
||||
if (wantsToFork != null) {
|
||||
@@ -585,14 +586,19 @@ private fun BoostWithDialog(
|
||||
}
|
||||
}
|
||||
|
||||
NewPostView(
|
||||
onClose = { wantsToFork = null },
|
||||
baseReplyTo = replyTo,
|
||||
fork = wantsToFork,
|
||||
version = (editState.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
val route =
|
||||
buildNewPostRoute(
|
||||
quote = wantsToQuote?.idHex,
|
||||
baseReplyTo = replyTo?.idHex,
|
||||
fork = wantsToFork?.idHex,
|
||||
version =
|
||||
(editState.value as? GenericLoadable.Loaded)
|
||||
?.loaded
|
||||
?.modificationToShow
|
||||
?.value
|
||||
?.idHex,
|
||||
)
|
||||
nav.nav(route)
|
||||
}
|
||||
|
||||
BoostReaction(
|
||||
@@ -614,13 +620,12 @@ private fun ReplyReactionWithDialog(
|
||||
var wantsToReplyTo by remember { mutableStateOf<Note?>(null) }
|
||||
|
||||
if (wantsToReplyTo != null) {
|
||||
NewPostView(
|
||||
onClose = { wantsToReplyTo = null },
|
||||
baseReplyTo = wantsToReplyTo,
|
||||
quote = null,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
val route =
|
||||
buildNewPostRoute(
|
||||
baseReplyTo = wantsToReplyTo?.idHex,
|
||||
quote = null,
|
||||
)
|
||||
nav.nav(route)
|
||||
}
|
||||
|
||||
ReplyReaction(baseNote, grayTint, accountViewModel) { wantsToReplyTo = baseNote }
|
||||
@@ -1014,20 +1019,26 @@ fun ZapReaction(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple24dp,
|
||||
onClick = {
|
||||
zapClick(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
context,
|
||||
onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } },
|
||||
onMultipleChoices = { wantsToZap = true },
|
||||
onError = { _, message ->
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog = showErrorMessageDialog + message
|
||||
}
|
||||
},
|
||||
onPayViaIntent = { wantsToPay = it },
|
||||
)
|
||||
scope.launch {
|
||||
zapClick(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
context,
|
||||
onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } },
|
||||
onMultipleChoices = {
|
||||
scope.launch {
|
||||
wantsToZap = true
|
||||
}
|
||||
},
|
||||
onError = { _, message ->
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog = showErrorMessageDialog + message
|
||||
}
|
||||
},
|
||||
onPayViaIntent = { wantsToPay = it },
|
||||
)
|
||||
}
|
||||
},
|
||||
onLongClick = { wantsToChangeZapAmount = true },
|
||||
onDoubleClick = { wantsToSetCustomZap = true },
|
||||
@@ -1043,8 +1054,10 @@ fun ZapReaction(
|
||||
zappingProgress = 0f
|
||||
},
|
||||
onChangeAmount = {
|
||||
wantsToZap = false
|
||||
wantsToChangeZapAmount = true
|
||||
scope.launch {
|
||||
wantsToZap = false
|
||||
wantsToChangeZapAmount = true
|
||||
}
|
||||
},
|
||||
onError = { _, message ->
|
||||
scope.launch {
|
||||
@@ -1249,7 +1262,6 @@ fun ObserveZapAmountText(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun BoostTypeChoicePopup(
|
||||
baseNote: Note,
|
||||
@@ -1259,62 +1271,121 @@ private fun BoostTypeChoicePopup(
|
||||
onQuote: () -> Unit,
|
||||
onRepost: () -> Unit,
|
||||
onFork: () -> Unit,
|
||||
) {
|
||||
val visibilityState = rememberVisibilityState(onDismiss)
|
||||
BoostTypeChoicePopup(
|
||||
baseNote,
|
||||
iconSize,
|
||||
accountViewModel,
|
||||
visibilityState,
|
||||
onQuote,
|
||||
onRepost,
|
||||
onFork,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun BoostTypeChoicePopup(
|
||||
baseNote: Note,
|
||||
iconSize: Dp,
|
||||
accountViewModel: AccountViewModel,
|
||||
visibilityState: MutableTransitionState<Boolean>,
|
||||
onQuote: () -> Unit,
|
||||
onRepost: () -> Unit,
|
||||
onFork: () -> Unit,
|
||||
) {
|
||||
val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() }
|
||||
|
||||
Popup(
|
||||
alignment = Alignment.BottomCenter,
|
||||
offset = IntOffset(0, iconSizePx),
|
||||
onDismissRequest = { onDismiss() },
|
||||
onDismissRequest = { visibilityState.targetState = false },
|
||||
properties = PopupProperties(focusable = true),
|
||||
) {
|
||||
FlowRow {
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
if (accountViewModel.isWriteable()) {
|
||||
accountViewModel.boost(baseNote)
|
||||
onDismiss()
|
||||
} else {
|
||||
onRepost()
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(stringRes(R.string.boost), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visibleState = visibilityState,
|
||||
enter = popupAnimationEnter,
|
||||
exit = popupAnimationExit,
|
||||
) {
|
||||
FlowRow {
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
if (accountViewModel.isWriteable()) {
|
||||
accountViewModel.boost(baseNote)
|
||||
visibilityState.targetState = false
|
||||
} else {
|
||||
onRepost()
|
||||
visibilityState.targetState = false
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(stringRes(R.string.boost), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = onQuote,
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(stringRes(R.string.quote), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = onQuote,
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(stringRes(R.string.quote), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = onFork,
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(stringRes(R.string.fork), color = Color.White, textAlign = TextAlign.Center)
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = onFork,
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(stringRes(R.string.fork), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val popupAnimationEnter: EnterTransition =
|
||||
slideInVertically(
|
||||
animationSpec = defaultTweenIntOffsetSpec,
|
||||
initialOffsetY = { it / 2 },
|
||||
) + fadeIn(animationSpec = defaultTweenFloatSpec)
|
||||
|
||||
val popupAnimationExit: ExitTransition =
|
||||
slideOutVertically(
|
||||
animationSpec = defaultTweenIntOffsetSpec,
|
||||
targetOffsetY = { it / 2 },
|
||||
) + fadeOut(animationSpec = defaultTweenFloatSpec)
|
||||
|
||||
@Composable
|
||||
fun rememberVisibilityState(onDismiss: () -> Unit): MutableTransitionState<Boolean> {
|
||||
// Prevent multiple calls to onDismiss()
|
||||
var dismissed by remember { mutableStateOf(false) }
|
||||
|
||||
val visibilityState = remember { MutableTransitionState(false).apply { targetState = true } }
|
||||
LaunchedEffect(visibilityState.targetState) {
|
||||
if (!visibilityState.targetState && !dismissed) {
|
||||
delay(defaultTweenDuration.toLong())
|
||||
dismissed = true
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
return visibilityState
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReactionChoicePopup(
|
||||
baseNote: Note,
|
||||
@@ -1322,6 +1393,18 @@ fun ReactionChoicePopup(
|
||||
accountViewModel: AccountViewModel,
|
||||
onDismiss: () -> Unit,
|
||||
onChangeAmount: () -> Unit,
|
||||
) {
|
||||
val visibilityState = rememberVisibilityState(onDismiss)
|
||||
ReactionChoicePopup(baseNote, iconSize, accountViewModel, visibilityState, onChangeAmount)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReactionChoicePopup(
|
||||
baseNote: Note,
|
||||
iconSize: Dp,
|
||||
accountViewModel: AccountViewModel,
|
||||
visibilityState: MutableTransitionState<Boolean>,
|
||||
onChangeAmount: () -> Unit,
|
||||
) {
|
||||
val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() }
|
||||
|
||||
@@ -1329,22 +1412,6 @@ fun ReactionChoicePopup(
|
||||
.collectAsStateWithLifecycle()
|
||||
val toRemove = remember { baseNote.reactedBy(accountViewModel.userProfile()).toImmutableSet() }
|
||||
|
||||
// Define animation specs
|
||||
val animationDuration = 250
|
||||
val fadeAnimationSpec = tween<Float>(durationMillis = animationDuration)
|
||||
|
||||
// Prevent multiple calls to onDismiss()
|
||||
var dismissed by remember { mutableStateOf(false) }
|
||||
|
||||
val visibilityState = remember { MutableTransitionState(false).apply { targetState = true } }
|
||||
LaunchedEffect(visibilityState.targetState) {
|
||||
if (!visibilityState.targetState && !dismissed) {
|
||||
delay(animationDuration.toLong())
|
||||
dismissed = true
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
Popup(
|
||||
alignment = Alignment.BottomCenter,
|
||||
offset = IntOffset(0, iconSizePx),
|
||||
@@ -1353,14 +1420,8 @@ fun ReactionChoicePopup(
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visibleState = visibilityState,
|
||||
enter =
|
||||
slideInVertically(
|
||||
initialOffsetY = { it / 2 },
|
||||
) + fadeIn(animationSpec = fadeAnimationSpec),
|
||||
exit =
|
||||
slideOutVertically(
|
||||
targetOffsetY = { it / 2 },
|
||||
) + fadeOut(animationSpec = fadeAnimationSpec),
|
||||
enter = popupAnimationEnter,
|
||||
exit = popupAnimationExit,
|
||||
) {
|
||||
ReactionChoicePopupContent(
|
||||
reactions,
|
||||
@@ -1518,7 +1579,6 @@ fun ZapAmountChoicePopup(
|
||||
ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, onDismiss, onChangeAmount, onError, onProgress, onPayViaIntent)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ZapAmountChoicePopup(
|
||||
baseNote: Note,
|
||||
@@ -1530,28 +1590,29 @@ fun ZapAmountChoicePopup(
|
||||
onError: (title: String, text: String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
|
||||
) {
|
||||
val visibilityState = rememberVisibilityState(onDismiss)
|
||||
ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, visibilityState, onChangeAmount, onError, onProgress, onPayViaIntent)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ZapAmountChoicePopup(
|
||||
baseNote: Note,
|
||||
zapAmountChoices: ImmutableList<Long>,
|
||||
accountViewModel: AccountViewModel,
|
||||
popupYOffset: Dp,
|
||||
visibilityState: MutableTransitionState<Boolean>,
|
||||
onChangeAmount: () -> Unit,
|
||||
onError: (title: String, text: String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val zapMessage = ""
|
||||
|
||||
val yOffset = with(LocalDensity.current) { -popupYOffset.toPx().toInt() }
|
||||
|
||||
// Define animation specs
|
||||
val animationDuration = 250
|
||||
val fadeAnimationSpec = tween<Float>(durationMillis = animationDuration)
|
||||
|
||||
// Prevent multiple calls to onDismiss()
|
||||
var dismissed by remember { mutableStateOf(false) }
|
||||
|
||||
val visibilityState = remember { MutableTransitionState(false).apply { targetState = true } }
|
||||
LaunchedEffect(visibilityState.targetState) {
|
||||
if (!visibilityState.targetState && !dismissed) {
|
||||
delay(animationDuration.toLong())
|
||||
dismissed = true
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
Popup(
|
||||
alignment = Alignment.BottomCenter,
|
||||
offset = IntOffset(0, yOffset),
|
||||
@@ -1560,14 +1621,8 @@ fun ZapAmountChoicePopup(
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visibleState = visibilityState,
|
||||
enter =
|
||||
slideInVertically(
|
||||
initialOffsetY = { it / 2 },
|
||||
) + fadeIn(animationSpec = fadeAnimationSpec),
|
||||
exit =
|
||||
slideOutVertically(
|
||||
targetOffsetY = { it / 2 },
|
||||
) + fadeOut(animationSpec = fadeAnimationSpec),
|
||||
enter = popupAnimationEnter,
|
||||
exit = popupAnimationExit,
|
||||
) {
|
||||
FlowRow(horizontalArrangement = Arrangement.Center) {
|
||||
zapAmountChoices.forEach { amountInSats ->
|
||||
|
||||
+2
-2
@@ -77,13 +77,13 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.service.firstFullChar
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@@ -86,9 +86,9 @@ import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.getFragmentActivity
|
||||
|
||||
@@ -73,8 +73,8 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
@@ -61,13 +61,13 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.PostButton
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.ZappedIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@@ -43,10 +43,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.EditPostView
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.buildNewPostRoute
|
||||
import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.externalLinkForNote
|
||||
import com.vitorpamplona.amethyst.ui.note.types.EditState
|
||||
@@ -124,11 +124,6 @@ fun NoteDropDownMenu(
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
val wantsToEditDraft =
|
||||
remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (wantsToEditPost.value) {
|
||||
// avoids changing while drafting a note and a new event shows up.
|
||||
val versionLookingAt =
|
||||
@@ -148,18 +143,6 @@ fun NoteDropDownMenu(
|
||||
)
|
||||
}
|
||||
|
||||
if (wantsToEditDraft.value) {
|
||||
NewPostView(
|
||||
onClose = {
|
||||
onDismiss()
|
||||
wantsToEditDraft.value = false
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
draft = note,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = true,
|
||||
onDismissRequest = onDismiss,
|
||||
@@ -243,7 +226,11 @@ fun NoteDropDownMenu(
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.edit_draft)) },
|
||||
onClick = {
|
||||
wantsToEditDraft.value = true
|
||||
val route =
|
||||
buildNewPostRoute(
|
||||
draft = note.idHex,
|
||||
)
|
||||
nav.nav(route)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+19
-1
@@ -1267,7 +1267,7 @@ class AccountViewModel(
|
||||
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||
Log.d(
|
||||
"Rendering Metrics",
|
||||
"Update feeds ${this@AccountViewModel} for ${account.userProfile().toBestDisplayName()}",
|
||||
"Update feeds ${this@AccountViewModel} for ${account.userProfile().toBestDisplayName()} with ${newNotes.size} new notes",
|
||||
)
|
||||
feedStates.updateFeedsWith(newNotes)
|
||||
upgradeAttestations()
|
||||
@@ -1699,3 +1699,21 @@ fun mockAccountViewModel(): AccountViewModel {
|
||||
sharedPreferencesViewModel.sharedPrefs,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun mockVitorAccountViewModel(): AccountViewModel {
|
||||
val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel()
|
||||
sharedPreferencesViewModel.init()
|
||||
|
||||
return AccountViewModel(
|
||||
AccountSettings(
|
||||
// blank keys
|
||||
keyPair =
|
||||
KeyPair(
|
||||
pubKey = Hex.decode("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
forcePubKeyCheck = false,
|
||||
),
|
||||
),
|
||||
sharedPreferencesViewModel.sharedPrefs,
|
||||
)
|
||||
}
|
||||
|
||||
+318
-293
@@ -18,12 +18,14 @@
|
||||
* 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.actions
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import android.util.Size
|
||||
import android.widget.Toast
|
||||
@@ -42,7 +44,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||
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
|
||||
@@ -119,9 +121,7 @@ import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.core.util.Consumer
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil.compose.AsyncImage
|
||||
@@ -134,13 +134,23 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPollOption
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPollVoteValueRange
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.RelaySelectionDialog
|
||||
import com.vitorpamplona.amethyst.ui.actions.ServerOption
|
||||
import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.getPhotoUri
|
||||
import com.vitorpamplona.amethyst.ui.components.BechLink
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.InvoiceRequest
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview
|
||||
import com.vitorpamplona.amethyst.ui.components.VideoView
|
||||
import com.vitorpamplona.amethyst.ui.components.ZapRaiserRequest
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Nav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.getActivity
|
||||
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.CancelIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.CloseIcon
|
||||
@@ -150,9 +160,6 @@ import com.vitorpamplona.amethyst.ui.note.PollIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.RegularPostIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapSplitIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.MyTextField
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ShowUserSuggestionList
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
@@ -190,8 +197,9 @@ import java.lang.Math.round
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class)
|
||||
@Composable
|
||||
fun NewPostView(
|
||||
onClose: () -> Unit,
|
||||
fun NewPostScreen(
|
||||
message: String? = null,
|
||||
attachment: Uri? = null,
|
||||
baseReplyTo: Note? = null,
|
||||
quote: Note? = null,
|
||||
fork: Note? = null,
|
||||
@@ -199,18 +207,19 @@ fun NewPostView(
|
||||
draft: Note? = null,
|
||||
enableMessageInterface: Boolean = false,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
nav: Nav,
|
||||
) {
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val postViewModel: NewPostViewModel = viewModel()
|
||||
postViewModel.wantsDirectMessage = enableMessageInterface
|
||||
|
||||
val context = LocalContext.current
|
||||
val activity = context.getActivity()
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
val scope = rememberCoroutineScope()
|
||||
var showRelaysDialog by remember { mutableStateOf(false) }
|
||||
var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() }
|
||||
|
||||
var showCamera by remember {
|
||||
mutableStateOf(true)
|
||||
}
|
||||
@@ -229,6 +238,12 @@ fun NewPostView(
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,20 +256,84 @@ fun NewPostView(
|
||||
}
|
||||
}
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = {
|
||||
scope.launch {
|
||||
postViewModel.sendDraftSync(relayList = relayList)
|
||||
onClose()
|
||||
DisposableEffect(nav, activity) {
|
||||
// Microsoft's swift key sends Gifs as new actions
|
||||
|
||||
val consumer =
|
||||
Consumer<Intent> { intent ->
|
||||
if (intent.action == Intent.ACTION_SEND) {
|
||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.ifBlank { null }?.let {
|
||||
postViewModel.addToMessage(it)
|
||||
}
|
||||
|
||||
(intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
|
||||
postViewModel.selectImage(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activity.addOnNewIntentListener(consumer)
|
||||
onDispose { activity.removeOnNewIntentListener(consumer) }
|
||||
}
|
||||
|
||||
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 = stringRes(id = R.string.relay_list_selector),
|
||||
modifier = Modifier.height(25.dp),
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
}
|
||||
PostButton(
|
||||
onPost = {
|
||||
postViewModel.sendPost(relayList = relayList)
|
||||
scope.launch {
|
||||
delay(100)
|
||||
nav.popBack()
|
||||
}
|
||||
},
|
||||
isActive = postViewModel.canPost(),
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
Row {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
CloseButton(
|
||||
onPress = {
|
||||
scope.launch {
|
||||
postViewModel.sendDraftSync(relayList = relayList)
|
||||
postViewModel.cancel()
|
||||
delay(100)
|
||||
nav.popBack()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
colors =
|
||||
TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
),
|
||||
)
|
||||
},
|
||||
properties =
|
||||
DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
dismissOnClickOutside = false,
|
||||
decorFitsSystemWindows = false,
|
||||
),
|
||||
) {
|
||||
) { pad ->
|
||||
if (showRelaysDialog) {
|
||||
RelaySelectionDialog(
|
||||
preSelectedList = relayList,
|
||||
@@ -264,308 +343,254 @@ fun NewPostView(
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
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 = stringRes(id = R.string.relay_list_selector),
|
||||
modifier = Modifier.height(25.dp),
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
}
|
||||
PostButton(
|
||||
onPost = {
|
||||
postViewModel.sendPost(relayList = relayList)
|
||||
scope.launch {
|
||||
delay(100)
|
||||
onClose()
|
||||
}
|
||||
},
|
||||
isActive = postViewModel.canPost(),
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
Row {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
CloseButton(
|
||||
onPress = {
|
||||
scope.launch {
|
||||
postViewModel.sendDraftSync(relayList = relayList)
|
||||
postViewModel.cancel()
|
||||
delay(100)
|
||||
onClose()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
colors =
|
||||
TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { pad ->
|
||||
Surface(
|
||||
Surface(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(pad)
|
||||
.consumeWindowInsets(pad)
|
||||
.imePadding(),
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(
|
||||
start = Size10dp,
|
||||
top = pad.calculateTopPadding(),
|
||||
end = Size10dp,
|
||||
bottom = pad.calculateBottomPadding(),
|
||||
).fillMaxSize(),
|
||||
Modifier.fillMaxSize(),
|
||||
) {
|
||||
Column(
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(),
|
||||
.padding(
|
||||
start = Size10dp,
|
||||
end = Size10dp,
|
||||
).weight(1f),
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.imePadding()
|
||||
.weight(1f),
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(scrollState),
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(scrollState),
|
||||
postViewModel.originalNote?.let {
|
||||
Row {
|
||||
NoteCompose(
|
||||
baseNote = it,
|
||||
modifier = MaterialTheme.colorScheme.replyModifier,
|
||||
isQuotedNote = true,
|
||||
unPackReply = false,
|
||||
makeItShort = true,
|
||||
quotesLeft = 1,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
Notifying(postViewModel.pTags?.toImmutableList()) {
|
||||
postViewModel.removeFromReplyList(it)
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.wantsDirectMessage) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
postViewModel.originalNote?.let {
|
||||
Row(Modifier.heightIn(max = 200.dp)) {
|
||||
NoteCompose(
|
||||
baseNote = it,
|
||||
modifier = MaterialTheme.colorScheme.replyModifier,
|
||||
isQuotedNote = true,
|
||||
unPackReply = false,
|
||||
makeItShort = true,
|
||||
quotesLeft = 1,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
SendDirectMessageTo(postViewModel = postViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.wantsProduct) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
SellProduct(postViewModel = postViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(vertical = Size10dp),
|
||||
) {
|
||||
BaseUserPicture(
|
||||
accountViewModel.userProfile(),
|
||||
Size35dp,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
MessageField(postViewModel)
|
||||
}
|
||||
|
||||
if (postViewModel.wantsPoll) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
PollField(postViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
val myUrlPreview = postViewModel.urlPreview
|
||||
if (myUrlPreview != null) {
|
||||
Row(modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
|
||||
if (RichTextParser.isValidURL(myUrlPreview)) {
|
||||
if (RichTextParser.isImageUrl(myUrlPreview)) {
|
||||
AsyncImage(
|
||||
model = myUrlPreview,
|
||||
contentDescription = myUrlPreview,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = 4.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.subtleBorder,
|
||||
QuoteBorder,
|
||||
),
|
||||
)
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
} else if (RichTextParser.isVideoUrl(myUrlPreview)) {
|
||||
VideoView(
|
||||
myUrlPreview,
|
||||
mimeType = null,
|
||||
roundedCorner = true,
|
||||
isFiniteHeight = false,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
} else {
|
||||
LoadUrlPreview(myUrlPreview, myUrlPreview, null, accountViewModel)
|
||||
}
|
||||
}
|
||||
} else if (RichTextParser.startsWithNIP19Scheme(myUrlPreview)) {
|
||||
val bgColor = MaterialTheme.colorScheme.background
|
||||
val backgroundColor = remember { mutableStateOf(bgColor) }
|
||||
|
||||
Row {
|
||||
Notifying(postViewModel.pTags?.toImmutableList()) {
|
||||
postViewModel.removeFromReplyList(it)
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.wantsDirectMessage) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
SendDirectMessageTo(postViewModel = postViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.wantsProduct) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
SellProduct(postViewModel = postViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(vertical = Size10dp),
|
||||
) {
|
||||
BaseUserPicture(
|
||||
accountViewModel.userProfile(),
|
||||
Size35dp,
|
||||
BechLink(
|
||||
word = myUrlPreview,
|
||||
canPreview = true,
|
||||
quotesLeft = 1,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
MessageField(postViewModel)
|
||||
} else if (RichTextParser.isUrlWithoutScheme(myUrlPreview)) {
|
||||
LoadUrlPreview("https://$myUrlPreview", myUrlPreview, null, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.wantsPoll) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
PollField(postViewModel)
|
||||
}
|
||||
}
|
||||
if (postViewModel.wantsToMarkAsSensitive) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
ContentSensitivityExplainer(postViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
val myUrlPreview = postViewModel.urlPreview
|
||||
if (myUrlPreview != null) {
|
||||
Row(modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
|
||||
if (RichTextParser.isValidURL(myUrlPreview)) {
|
||||
if (RichTextParser.isImageUrl(myUrlPreview)) {
|
||||
AsyncImage(
|
||||
model = myUrlPreview,
|
||||
contentDescription = myUrlPreview,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = 4.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.subtleBorder,
|
||||
QuoteBorder,
|
||||
),
|
||||
)
|
||||
} else if (RichTextParser.isVideoUrl(myUrlPreview)) {
|
||||
VideoView(
|
||||
myUrlPreview,
|
||||
mimeType = null,
|
||||
roundedCorner = true,
|
||||
isFiniteHeight = false,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
} else {
|
||||
LoadUrlPreview(myUrlPreview, myUrlPreview, null, accountViewModel)
|
||||
}
|
||||
} else if (RichTextParser.startsWithNIP19Scheme(myUrlPreview)) {
|
||||
val bgColor = MaterialTheme.colorScheme.background
|
||||
val backgroundColor = remember { mutableStateOf(bgColor) }
|
||||
if (postViewModel.wantsToAddGeoHash) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
LocationAsHash(postViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
BechLink(
|
||||
word = myUrlPreview,
|
||||
canPreview = true,
|
||||
quotesLeft = 1,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
} else if (RichTextParser.isUrlWithoutScheme(myUrlPreview)) {
|
||||
LoadUrlPreview("https://$myUrlPreview", myUrlPreview, null, accountViewModel)
|
||||
if (postViewModel.wantsForwardZapTo) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(top = Size5dp, bottom = Size5dp, start = Size10dp),
|
||||
) {
|
||||
FowardZapTo(postViewModel, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
val url = postViewModel.contentToAddUrl
|
||||
if (url != null) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
ImageVideoDescription(
|
||||
url,
|
||||
accountViewModel.account.settings.defaultFileServer,
|
||||
onAdd = { alt, server, sensitiveContent, mediaQuality ->
|
||||
postViewModel.upload(url, alt, sensitiveContent, mediaQuality, false, server, accountViewModel::toast, context)
|
||||
if (!server.isNip95) {
|
||||
accountViewModel.account.settings.changeDefaultFileServer(server.server)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onCancel = { postViewModel.contentToAddUrl = null },
|
||||
onError = { scope.launch { Toast.makeText(context, context.resources.getText(it), Toast.LENGTH_SHORT).show() } },
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.wantsToMarkAsSensitive) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
ContentSensitivityExplainer(postViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.wantsToAddGeoHash) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
LocationAsHash(postViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.wantsForwardZapTo) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(top = Size5dp, bottom = Size5dp, start = Size10dp),
|
||||
) {
|
||||
FowardZapTo(postViewModel, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
val url = postViewModel.contentToAddUrl
|
||||
if (url != null) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
ImageVideoDescription(
|
||||
url,
|
||||
accountViewModel.account.settings.defaultFileServer,
|
||||
onAdd = { alt, server, sensitiveContent, mediaQuality ->
|
||||
postViewModel.upload(url, alt, sensitiveContent, mediaQuality, false, server, accountViewModel::toast, context)
|
||||
if (!server.isNip95) {
|
||||
accountViewModel.account.settings.changeDefaultFileServer(server.server)
|
||||
}
|
||||
if (postViewModel.wantsInvoice) {
|
||||
postViewModel.lnAddress()?.let { lud16 ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
InvoiceRequest(
|
||||
lud16,
|
||||
accountViewModel.account.userProfile().pubkeyHex,
|
||||
accountViewModel,
|
||||
stringRes(id = R.string.lightning_invoice),
|
||||
stringRes(id = R.string.lightning_create_and_add_invoice),
|
||||
onSuccess = {
|
||||
postViewModel.insertAtCursor(it)
|
||||
postViewModel.wantsInvoice = false
|
||||
},
|
||||
onCancel = { postViewModel.contentToAddUrl = null },
|
||||
onError = { scope.launch { Toast.makeText(context, context.resources.getText(it), Toast.LENGTH_SHORT).show() } },
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.wantsInvoice) {
|
||||
postViewModel.lnAddress()?.let { lud16 ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
InvoiceRequest(
|
||||
lud16,
|
||||
accountViewModel.account.userProfile().pubkeyHex,
|
||||
accountViewModel,
|
||||
stringRes(id = R.string.lightning_invoice),
|
||||
stringRes(id = R.string.lightning_create_and_add_invoice),
|
||||
onSuccess = {
|
||||
postViewModel.insertAtCursor(it)
|
||||
postViewModel.wantsInvoice = false
|
||||
},
|
||||
onClose = { postViewModel.wantsInvoice = false },
|
||||
onError = { title, message -> accountViewModel.toast(title, message) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.wantsZapraiser && postViewModel.hasLnAddress()) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
ZapRaiserRequest(
|
||||
stringRes(id = R.string.zapraiser),
|
||||
postViewModel,
|
||||
onClose = { postViewModel.wantsInvoice = false },
|
||||
onError = { title, message -> accountViewModel.toast(title, message) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ShowUserSuggestionList(
|
||||
postViewModel,
|
||||
accountViewModel,
|
||||
modifier = Modifier.heightIn(0.dp, 300.dp),
|
||||
)
|
||||
|
||||
BottomRowActions(postViewModel)
|
||||
if (postViewModel.wantsZapraiser && postViewModel.hasLnAddress()) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
ZapRaiserRequest(
|
||||
stringRes(id = R.string.zapraiser),
|
||||
postViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ShowUserSuggestionList(
|
||||
postViewModel,
|
||||
accountViewModel,
|
||||
modifier = Modifier.heightIn(0.dp, 300.dp),
|
||||
)
|
||||
|
||||
BottomRowActions(postViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Dialog(
|
||||
// onDismissRequest = {
|
||||
// scope.launch {
|
||||
// postViewModel.sendDraftSync(relayList = relayList)
|
||||
// onClose()
|
||||
// }
|
||||
// },
|
||||
// properties =
|
||||
// DialogProperties(
|
||||
// usePlatformDefaultWidth = false,
|
||||
// dismissOnClickOutside = false,
|
||||
// decorFitsSystemWindows = false,
|
||||
// ),
|
||||
// ) {
|
||||
// }
|
||||
}
|
||||
|
||||
@Composable
|
||||
+6
-15
@@ -49,8 +49,8 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewChannelView
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.buildNewPostRoute
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font12SP
|
||||
@@ -63,25 +63,12 @@ fun ChannelFabColumn(
|
||||
) {
|
||||
var isOpen by remember { mutableStateOf(false) }
|
||||
|
||||
var wantsToSendNewMessage by remember { mutableStateOf(false) }
|
||||
|
||||
var wantsToCreateChannel by remember { mutableStateOf(false) }
|
||||
|
||||
if (wantsToCreateChannel) {
|
||||
NewChannelView({ wantsToCreateChannel = false }, accountViewModel = accountViewModel)
|
||||
}
|
||||
|
||||
if (wantsToSendNewMessage) {
|
||||
NewPostView(
|
||||
onClose = { wantsToSendNewMessage = false },
|
||||
enableMessageInterface = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
// JoinUserOrChannelView({ wantsToJoinChannelOrUser = false }, accountViewModel =
|
||||
// accountViewModel, nav = nav)
|
||||
}
|
||||
|
||||
Column {
|
||||
AnimatedVisibility(
|
||||
visible = isOpen,
|
||||
@@ -91,7 +78,11 @@ fun ChannelFabColumn(
|
||||
Column {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
wantsToSendNewMessage = true
|
||||
val route =
|
||||
buildNewPostRoute(
|
||||
enableMessageInterface = true,
|
||||
)
|
||||
nav.nav(route)
|
||||
isOpen = false
|
||||
},
|
||||
modifier = Size55Modifier,
|
||||
|
||||
+3
-3
@@ -85,10 +85,8 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.PostButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.ServerOption
|
||||
import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
@@ -107,7 +105,9 @@ import com.vitorpamplona.amethyst.ui.note.elements.ObserveRelayListForDMs
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.ObserveRelayListForDMsAndDisplayIfNotFound
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.UserLine
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
|
||||
@@ -387,7 +387,7 @@ fun PrepareChatroomViewModels(
|
||||
}
|
||||
|
||||
if (draftMessage != null) {
|
||||
LaunchedEffect(key1 = draftMessage) { newPostModel.message = TextFieldValue(draftMessage) }
|
||||
LaunchedEffect(key1 = draftMessage) { newPostModel.updateMessage(TextFieldValue(draftMessage)) }
|
||||
}
|
||||
|
||||
ChatroomScreen(
|
||||
|
||||
+8
-10
@@ -27,8 +27,6 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -36,9 +34,9 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.buildNewPostRoute
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
@@ -60,14 +58,14 @@ fun NewCommunityNoteButton(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
var wantsToPost by remember { mutableStateOf(false) }
|
||||
|
||||
if (wantsToPost) {
|
||||
NewPostView({ wantsToPost = false }, note, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = { wantsToPost = true },
|
||||
onClick = {
|
||||
val route =
|
||||
buildNewPostRoute(
|
||||
baseReplyTo = note.idHex,
|
||||
)
|
||||
nav.nav(route)
|
||||
},
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
|
||||
+5
-10
@@ -27,16 +27,14 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.buildNewPostRoute
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
@@ -46,14 +44,11 @@ fun NewNoteButton(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
var wantsToPost by remember { mutableStateOf(false) }
|
||||
|
||||
if (wantsToPost) {
|
||||
NewPostView({ wantsToPost = false }, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = { wantsToPost = true },
|
||||
onClick = {
|
||||
val route = buildNewPostRoute()
|
||||
nav.nav(route)
|
||||
},
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
|
||||
+2
@@ -26,6 +26,7 @@ import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
@@ -43,6 +44,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
class SearchBarViewModel(
|
||||
val account: Account,
|
||||
) : ViewModel() {
|
||||
val focusRequester = FocusRequester()
|
||||
var searchValue by mutableStateOf("")
|
||||
|
||||
private var _searchResultsUsers = MutableStateFlow<List<User>>(emptyList())
|
||||
|
||||
+5
-12
@@ -52,7 +52,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
@@ -91,7 +90,6 @@ import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
@@ -148,6 +146,10 @@ fun SearchScreen(
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LaunchedEffect(searchBarViewModel.focusRequester) {
|
||||
searchBarViewModel.focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
DisappearingScaffold(
|
||||
isInvertedLayout = false,
|
||||
topBar = {
|
||||
@@ -234,15 +236,6 @@ private fun SearchTextField(
|
||||
modifier: Modifier,
|
||||
onTextChanges: (String) -> Unit,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
delay(100)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier.padding(10.dp).fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@@ -264,7 +257,7 @@ private fun SearchTextField(
|
||||
Modifier
|
||||
.weight(1f, true)
|
||||
.defaultMinSize(minHeight = 20.dp)
|
||||
.focusRequester(focusRequester),
|
||||
.focusRequester(searchBarViewModel.focusRequester),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.npub_hex_username),
|
||||
|
||||
+1
-1
@@ -33,13 +33,13 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.rememberHeightDecreaser
|
||||
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountContent
|
||||
import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
+22
-15
@@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -196,9 +197,9 @@ import com.vitorpamplona.quartz.events.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.events.TorrentEvent
|
||||
import com.vitorpamplona.quartz.events.VideoEvent
|
||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -235,26 +236,32 @@ fun RenderThreadFeed(
|
||||
nav: INav,
|
||||
) {
|
||||
val items by loaded.feed.collectAsStateWithLifecycle()
|
||||
val firstTimeScrolled = remember { TimeUtils.now() }
|
||||
|
||||
LaunchedEffect(noteId) {
|
||||
// waits to load the thread to scroll to item.
|
||||
delay(100)
|
||||
val noteForPosition =
|
||||
items.list
|
||||
.filter { it.idHex == noteId }
|
||||
.firstOrNull()
|
||||
var position = items.list.indexOf(noteForPosition)
|
||||
|
||||
if (position >= 0) {
|
||||
if (position >= 1 && position < items.list.size - 1) {
|
||||
position-- // show the replying note
|
||||
LaunchedEffect(noteId, items.list) {
|
||||
// hack to allow multiple scrolls to Item while posts on the screen load.
|
||||
// This is important when clicking on a reply of an older thread in Notifications
|
||||
// In that case, this screen will open with 0-1 items, and the scrollToItem below
|
||||
// will not change the state of the screen (too few items, scroll is not available)
|
||||
// as the app loads the reaming of the thread the position of the reply changes
|
||||
// and becuase there wasn't a possibility to scroll before and now there is one,
|
||||
// the screen stays at the top. Once the thread has enough replies, the lazy column
|
||||
// updates with new items correctly. It just needs a few items to start the scrool.
|
||||
//
|
||||
// This hack allows the list 1 second to fill up with more
|
||||
// records before setting up the position on the feed.
|
||||
//
|
||||
// It jumps around, but it is the best we can do.
|
||||
if (TimeUtils.now() - firstTimeScrolled < 1000) {
|
||||
val position = items.list.indexOfFirst { it.idHex == noteId }
|
||||
if (position >= 0) {
|
||||
listState.scrollToItem(position, -200)
|
||||
}
|
||||
|
||||
listState.scrollToItem(position)
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = FeedPadding,
|
||||
state = listState,
|
||||
) {
|
||||
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
/**
|
||||
* 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.screen.loggedIn.video
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.compose.foundation.MutatePriority
|
||||
import androidx.compose.foundation.gestures.ScrollScope
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.listSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* This file only exists to fix an interference between the Disappearing Top
|
||||
* and Bottom Scaffold bars and the offsetFraction of the pager. The current
|
||||
* implementation ends the scroll at a state where some fraction is still present
|
||||
* which places videos away from their natural position in the page.
|
||||
*
|
||||
* The fix simply animates it back to the fraction = 0.
|
||||
*/
|
||||
|
||||
@Composable
|
||||
fun myRememberPagerState(
|
||||
initialPage: Int = 0,
|
||||
@FloatRange(from = -0.5, to = 0.5) initialPageOffsetFraction: Float = 0f,
|
||||
pageCount: () -> Int,
|
||||
): PagerState =
|
||||
rememberSaveable(saver = DefaultPagerState.Saver) {
|
||||
DefaultPagerState(
|
||||
initialPage,
|
||||
initialPageOffsetFraction,
|
||||
pageCount,
|
||||
)
|
||||
}.apply {
|
||||
pageCountState.value = pageCount
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun myRememberForeverPagerState(
|
||||
key: String,
|
||||
initialFirstVisibleItemIndex: Int = 0,
|
||||
initialFirstVisibleItemScrollOffset: Float = 0.0f,
|
||||
pageCount: () -> Int,
|
||||
): PagerState =
|
||||
rememberForeverPagerState(key, initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset, pageCount) { initialPage, initialPageOffsetFraction, pageCount ->
|
||||
myRememberPagerState(initialPage, initialPageOffsetFraction, pageCount)
|
||||
}
|
||||
|
||||
private class DefaultPagerState(
|
||||
currentPage: Int,
|
||||
currentPageOffsetFraction: Float,
|
||||
updatedPageCount: () -> Int,
|
||||
) : PagerState(currentPage, currentPageOffsetFraction) {
|
||||
var pageCountState = mutableStateOf(updatedPageCount)
|
||||
override val pageCount: Int get() = pageCountState.value.invoke()
|
||||
|
||||
override suspend fun scroll(
|
||||
scrollPriority: MutatePriority,
|
||||
block: suspend ScrollScope.() -> Unit,
|
||||
) {
|
||||
super.scroll(scrollPriority, block)
|
||||
if (abs(currentPageOffsetFraction) > 0) {
|
||||
animateScrollToPage(currentPage, 0f)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* To keep current page and current page offset saved
|
||||
*/
|
||||
val Saver: Saver<DefaultPagerState, *> =
|
||||
listSaver(
|
||||
save = {
|
||||
listOf(
|
||||
it.currentPage,
|
||||
(it.currentPageOffsetFraction).coerceIn(-0.5f, 0.5f),
|
||||
it.pageCount,
|
||||
)
|
||||
},
|
||||
restore = {
|
||||
DefaultPagerState(
|
||||
currentPage = it[0] as Int,
|
||||
currentPageOffsetFraction = it[1] as Float,
|
||||
updatedPageCount = { it[2] as Int },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
+31
-24
@@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.VerticalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -56,7 +57,6 @@ import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||
@@ -67,9 +67,11 @@ import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop
|
||||
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
|
||||
import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.buildNewPostRoute
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.BoostReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport
|
||||
@@ -245,9 +247,9 @@ fun SlidingCarousel(
|
||||
|
||||
val pagerState =
|
||||
if (pagerStateKey != null) {
|
||||
myRememberForeverPagerState(pagerStateKey, items.list.size) { items.list.size }
|
||||
rememberForeverPagerState(pagerStateKey, items.list.size) { items.list.size }
|
||||
} else {
|
||||
myRememberPagerState(items.list.size) { items.list.size }
|
||||
rememberPagerState(items.list.size) { items.list.size }
|
||||
}
|
||||
|
||||
WatchScrollToTop(videoFeedContentState, pagerState)
|
||||
@@ -391,29 +393,29 @@ fun ReactionsColumn(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
var wantsToReplyTo by remember { mutableStateOf<Note?>(null) }
|
||||
// var wantsToReplyTo by remember { mutableStateOf<Note?>(null) }
|
||||
|
||||
var wantsToQuote by remember { mutableStateOf<Note?>(null) }
|
||||
// var wantsToQuote by remember { mutableStateOf<Note?>(null) }
|
||||
|
||||
if (wantsToReplyTo != null) {
|
||||
NewPostView(
|
||||
onClose = { wantsToReplyTo = null },
|
||||
baseReplyTo = wantsToReplyTo,
|
||||
quote = null,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
// if (wantsToReplyTo != null) {
|
||||
// NewPostView(
|
||||
// onClose = { wantsToReplyTo = null },
|
||||
// baseReplyTo = wantsToReplyTo,
|
||||
// quote = null,
|
||||
// accountViewModel = accountViewModel,
|
||||
// nav = nav,
|
||||
// )
|
||||
// }
|
||||
|
||||
if (wantsToQuote != null) {
|
||||
NewPostView(
|
||||
onClose = { wantsToQuote = null },
|
||||
baseReplyTo = null,
|
||||
quote = wantsToQuote,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
// if (wantsToQuote != null) {
|
||||
// NewPostView(
|
||||
// onClose = { wantsToQuote = null },
|
||||
// baseReplyTo = null,
|
||||
// quote = wantsToQuote,
|
||||
// accountViewModel = accountViewModel,
|
||||
// nav = nav,
|
||||
// )
|
||||
// }
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
@@ -437,7 +439,12 @@ fun ReactionsColumn(
|
||||
iconSizeModifier = Size40Modifier,
|
||||
iconSize = Size40dp,
|
||||
onQuotePress = {
|
||||
wantsToQuote = baseNote
|
||||
// wantsToQuote = baseNote
|
||||
val route =
|
||||
buildNewPostRoute(
|
||||
quote = baseNote.idHex,
|
||||
)
|
||||
nav.nav(route)
|
||||
},
|
||||
onForkPress = {
|
||||
},
|
||||
|
||||
@@ -53,3 +53,6 @@ val DarkWarningColor = Color(0xFFF8DE22)
|
||||
|
||||
val LightAllGoodColor = Color(0xFF339900)
|
||||
val DarkAllGoodColor = Color(0xFF99cc33)
|
||||
|
||||
val LightFundraiserProgressColor = Color(0xFF3DB601)
|
||||
val DarkFundraiserProgressColor = Color(0xFF61A229)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.theme
|
||||
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -40,6 +41,7 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.Placeholder
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val Shapes =
|
||||
@@ -176,6 +178,9 @@ val UserNameMaxRowHeight = Modifier.fillMaxWidth()
|
||||
val Height24dpModifier = Modifier.height(24.dp)
|
||||
val Height4dpModifier = Modifier.height(4.dp)
|
||||
|
||||
val Height24dpFilledModifier = Modifier.fillMaxWidth().height(24.dp)
|
||||
val Height4dpFilledModifier = Modifier.fillMaxWidth().height(4.dp)
|
||||
|
||||
val AccountPictureModifier = Modifier.size(55.dp).clip(shape = CircleShape)
|
||||
val HeaderPictureModifier = Modifier.size(34.dp).clip(shape = CircleShape)
|
||||
|
||||
@@ -287,3 +292,7 @@ val reactionBox =
|
||||
.padding(5.dp)
|
||||
|
||||
val ripple24dp = ripple(bounded = false, radius = Size24dp)
|
||||
|
||||
val defaultTweenDuration = 100
|
||||
val defaultTweenFloatSpec = tween<Float>(durationMillis = defaultTweenDuration)
|
||||
val defaultTweenIntOffsetSpec = tween<IntOffset>(durationMillis = defaultTweenDuration)
|
||||
|
||||
@@ -397,6 +397,9 @@ val ColorScheme.warningColor: Color
|
||||
val ColorScheme.allGoodColor: Color
|
||||
get() = if (isLight) LightAllGoodColor else DarkAllGoodColor
|
||||
|
||||
val ColorScheme.fundraiserProgressColor: Color
|
||||
get() = if (isLight) LightFundraiserProgressColor else DarkFundraiserProgressColor
|
||||
|
||||
val ColorScheme.markdownStyle: RichTextStyle
|
||||
get() = if (isLight) MarkDownStyleOnLight else MarkDownStyleOnDark
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
@@ -520,6 +520,7 @@
|
||||
<string name="light">Clair</string>
|
||||
<string name="dark">Sombre</string>
|
||||
<string name="application_preferences">Préférences de l\'application</string>
|
||||
<string name="wallet_connect">Wallet Connect</string>
|
||||
<string name="language">Langage</string>
|
||||
<string name="theme">Thème</string>
|
||||
<string name="automatically_load_images_gifs">Prévisualisation des images</string>
|
||||
|
||||
@@ -520,6 +520,7 @@
|
||||
<string name="light">प्रकाशवान</string>
|
||||
<string name="dark">अन्धकारमय</string>
|
||||
<string name="application_preferences">क्रमक आद्यताएँ</string>
|
||||
<string name="wallet_connect">धनकोष संयोजन</string>
|
||||
<string name="language">भाषा</string>
|
||||
<string name="theme">प्रदर्शनशैली</string>
|
||||
<string name="automatically_load_images_gifs">चित्र पूर्वीक्षण</string>
|
||||
|
||||
@@ -520,6 +520,7 @@
|
||||
<string name="light">Licht</string>
|
||||
<string name="dark">Donker</string>
|
||||
<string name="application_preferences">Application preferences</string>
|
||||
<string name="wallet_connect">Wallet Connect</string>
|
||||
<string name="language">Taal</string>
|
||||
<string name="theme">Thema</string>
|
||||
<string name="automatically_load_images_gifs">Automatisch afbeeldingen/gifs laden</string>
|
||||
|
||||
@@ -520,6 +520,7 @@
|
||||
<string name="light">Jasny</string>
|
||||
<string name="dark">Ciemny</string>
|
||||
<string name="application_preferences">Ustawienia Aplikacji</string>
|
||||
<string name="wallet_connect">Podłącz portfel</string>
|
||||
<string name="language">Język</string>
|
||||
<string name="theme">Motyw</string>
|
||||
<string name="automatically_load_images_gifs">Podgląd obrazu</string>
|
||||
|
||||
@@ -9,6 +9,7 @@ Prijavi se s privatnim ključem za všečkanje sporočila</string>
|
||||
<string name="public_chat_feed">Javni vir novic</string>
|
||||
<string name="add_a_relay">Dodaj rele</string>
|
||||
<string name="display_name">Prikazno ime</string>
|
||||
<string name="uploading">Nalaganje …</string>
|
||||
<string name="npub_hex_username">"npub, uporabniško ime, tekst"</string>
|
||||
<string name="quick_action_share_browser_link">Deli povezavo brskalnika</string>
|
||||
<string name="quick_action_request_deletion_gallery_title">Izbris iz galerije</string>
|
||||
@@ -22,4 +23,14 @@ Prijavi se s privatnim ključem za všečkanje sporočila</string>
|
||||
<string name="ui_feature_set_type_complete">Končaj</string>
|
||||
<string name="mute_button">Zvok je prižgan. Klikni da ga utišaš</string>
|
||||
<string name="relay_information_document_error_failed_to_reach_server">Neuspela povezava z %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_parse_response">Neuspešna analiza odgovora %1$s: %2$s</string>
|
||||
<string name="cashu_failed_redemption">Cashu-ja ni bilo mogoče unovčiti</string>
|
||||
<string name="cashu_no_wallet_found">Na sistemu ni najdene združljive Cashu denarnice</string>
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">Prejemnikova \"lightning\" storitev na %1$s ni na voljo. Izračunana je bila iz \"lightning\" naslova \'%2$s\'. Napaka: %3$s. Preverite, ali strežnik deluje in ali je \"lightning\" naslov pravilen</string>
|
||||
<string name="error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup_with_user">Napaka pri razčlenjevanju JSON med pridobivanjem fakture iz %1$s. Preverite uporabnikove nastavitve za \"Lightning\"</string>
|
||||
<string name="select_push_server">Izberi \"UnifiedPush\" aplikacijo</string>
|
||||
<string name="push_server_install_app_description"> Za prejemanje potisnih obvestil namestite aplikacijo, ki podpira [Unified Push](https://unifiedpush.org/), na primer [Ntfy](https://ntfy.sh/).
|
||||
Po namestitvi izberite aplikacijo, ki jo želite uporabljati, v nastavitvah.
|
||||
</string>
|
||||
<string name="http_status_416">Razpon ni zadovoljiv – Strežnik ne more izpolniti vrednosti, navedene v \"request’s Range header\" polju. </string>
|
||||
</resources>
|
||||
|
||||
@@ -520,6 +520,7 @@
|
||||
<string name="light">浅色</string>
|
||||
<string name="dark">深色</string>
|
||||
<string name="application_preferences">应用程序首选项</string>
|
||||
<string name="wallet_connect">钱包绑定</string>
|
||||
<string name="language">语言</string>
|
||||
<string name="theme">主题</string>
|
||||
<string name="automatically_load_images_gifs">图像预览</string>
|
||||
|
||||
+2
@@ -62,6 +62,7 @@ import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -212,6 +213,7 @@ private fun TranslationMessage(
|
||||
MaterialTheme.colorScheme.onSurface.copy(
|
||||
alpha = 0.32f,
|
||||
),
|
||||
fontSize = Font14SP,
|
||||
),
|
||||
overflow = TextOverflow.Visible,
|
||||
maxLines = 3,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -393,17 +393,16 @@ class Relay(
|
||||
}
|
||||
|
||||
if (relayFilters.isNotEmpty()) {
|
||||
val request =
|
||||
writeToSocket(
|
||||
relayFilters.joinToStringLimited(
|
||||
separator = ",",
|
||||
limit = 20,
|
||||
limit = 19,
|
||||
prefix = """["REQ","$requestId",""",
|
||||
postfix = "]",
|
||||
) {
|
||||
it.filter.toJson(url)
|
||||
}
|
||||
|
||||
writeToSocket(request)
|
||||
},
|
||||
)
|
||||
|
||||
afterEOSEPerSubscription[requestId] = false
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
[versions]
|
||||
accompanistAdaptive = "0.34.0"
|
||||
activityCompose = "1.9.2"
|
||||
agp = "8.7.0"
|
||||
android-compileSdk = "34"
|
||||
activityCompose = "1.9.3"
|
||||
agp = "8.7.1"
|
||||
android-compileSdk = "35"
|
||||
android-minSdk = "26"
|
||||
android-targetSdk = "34"
|
||||
android-targetSdk = "35"
|
||||
androidKotlinGeohash = "1.0"
|
||||
androidxJunit = "1.2.1"
|
||||
appcompat = "1.7.0"
|
||||
audiowaveform = "1.1.1"
|
||||
benchmark = "1.3.2"
|
||||
benchmarkJunit4 = "1.3.2"
|
||||
benchmark = "1.3.3"
|
||||
benchmarkJunit4 = "1.3.3"
|
||||
biometricKtx = "1.2.0-alpha05"
|
||||
blurhash = "1.0.0"
|
||||
coil = "2.7.0"
|
||||
composeBom = "2024.09.03"
|
||||
composeBom = "2024.10.00"
|
||||
coreKtx = "1.13.1"
|
||||
espressoCore = "3.6.1"
|
||||
firebaseBom = "33.4.0"
|
||||
@@ -36,7 +36,7 @@ markdown = "077a2cde64"
|
||||
media3 = "1.4.1"
|
||||
mockk = "1.13.12"
|
||||
kotlinx-coroutines-test = "1.9.0-RC.2"
|
||||
navigationCompose = "2.8.2"
|
||||
navigationCompose = "2.8.3"
|
||||
okhttp = "5.0.0-alpha.14"
|
||||
runner = "1.6.2"
|
||||
rfc3986 = "0.1.0"
|
||||
|
||||
Reference in New Issue
Block a user