Compare commits

...
33 Commits
Author SHA1 Message Date
Vitor Pamplona f0b14edef6 v0.92.7 2024-10-19 14:17:42 -04:00
Vitor Pamplona aae2ffebd7 Fixes the bug of unable to decrypt mute lists. 2024-10-19 14:17:17 -04:00
Vitor Pamplona 7b11d0ce81 v0.92.6 2024-10-17 17:21:20 -04:00
Vitor Pamplona 26ba75c7c9 Fixes array too big because strfry blocks more than 20 filters 2024-10-17 17:18:35 -04:00
Vitor Pamplona 67fb3bf488 Fixes margin of poll options with translation 2024-10-17 15:22:34 -04:00
Vitor Pamplona 24b29396cb Reduces the font size for the translation label 2024-10-17 15:22:19 -04:00
Vitor Pamplona 25c3532637 Fixes poll rendering 2024-10-17 14:55:07 -04:00
Vitor Pamplona cff6ee457d Fixes zapraiser progress bar 2024-10-17 14:18:40 -04:00
Vitor Pamplona c0ba6f5b00 If the Share target intent opens with the new post screen already active, just updates the post instead of creating a new one. 2024-10-17 13:59:52 -04:00
Vitor Pamplona 44d8b8ff33 v0.92.5 2024-10-16 17:34:10 -04:00
Vitor Pamplona f495acb570 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-10-16 17:33:58 -04:00
Vitor PamplonaandGitHub ecfc8bcd87 Merge pull request #1144 from geovnn/main
Prevent clicks outside boost popup
2024-10-16 17:33:39 -04:00
Vitor Pamplona 724bea6bbb Updates compose, benchmark, navigation, and activity 2024-10-16 17:32:33 -04:00
Giovanni Gatti 6826db13a4 Prevent clicks outside boost popup 2024-10-16 23:11:51 +02:00
Vitor Pamplona c5dd2c5651 Fixes recreating the new post screen from intent when switching accounts. 2024-10-16 13:33:46 -04:00
Vitor Pamplona 236ff2d680 v0.92.4 2024-10-16 11:13:56 -04:00
Vitor Pamplona 13e2546d36 Makes Amethyst a share target for texts, images and videos. 2024-10-16 10:54:57 -04:00
Vitor Pamplona 549b9f53e5 Fixes new media post dialog for the edge to edge borders 2024-10-16 09:41:02 -04:00
Vitor Pamplona 974c022aed Fixes padding of the new new post screen 2024-10-15 17:20:16 -04:00
Vitor Pamplona a57566dd84 Changes the new post screen to use the non-disappearing version of the scaffold. 2024-10-15 16:55:09 -04:00
Vitor PamplonaandGitHub 72e86cf391 Merge pull request #1143 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-10-15 16:25:21 -04:00
Crowdin Bot 3b23cded56 New Crowdin translations by GitHub Action 2024-10-15 20:04:18 +00:00
Vitor Pamplona 121337eb83 Makes new post screen come in and out from the bottom 2024-10-15 16:02:20 -04:00
Vitor Pamplona fe87d642c9 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NewPostScreen.kt
2024-10-15 15:54:26 -04:00
Vitor Pamplona b8112ff1aa Correctly maps the write status of the outbox relays. 2024-10-15 15:53:00 -04:00
Vitor PamplonaandGitHub 1eee0bd1ae Merge pull request #1142 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-10-15 13:55:09 -04:00
Crowdin Bot f48335e3be New Crowdin translations by GitHub Action 2024-10-15 17:46:27 +00:00
Vitor PamplonaandGitHub fcc4befb22 Merge pull request #1141 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-10-15 13:44:54 -04:00
Crowdin Bot e28a1896ec New Crowdin translations by GitHub Action 2024-10-15 17:38:52 +00:00
Vitor PamplonaandGitHub f10e66e6a5 Merge pull request #1137 from geovnn/main
Change NewPostView to NewPostScreen to make it route based
2024-10-15 13:37:33 -04:00
Vitor PamplonaandGitHub 825a227e2d Merge pull request #1140 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-10-15 12:09:51 -04:00
Crowdin Bot 3446fdaf7f New Crowdin translations by GitHub Action 2024-10-15 16:08:45 +00:00
Giovanni Gatti 89de272ea1 Change NewPostView to NewPostScreen to make it route based 2024-10-14 23:19:43 +02:00
50 changed files with 1138 additions and 629 deletions
+2 -2
View File
@@ -16,8 +16,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk libs.versions.android.minSdk.get().toInteger()
targetSdk libs.versions.android.targetSdk.get().toInteger()
versionCode 408
versionName "0.92.3"
versionCode 412
versionName "0.92.7"
buildConfigField "String", "RELEASE_NOTES_ID", "\"98f2acaf9b3eb70d87607942e576f9a086ac28adf37869da9a29f4815cecac86\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+18
View File
@@ -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(
@@ -154,8 +154,9 @@ class EventNotificationConsumer(
acc: AccountSettings,
) {
if (
event.createdAt > TimeUtils.fifteenMinutesAgo() &&
// old event being re-broadcasted
event.createdAt > TimeUtils.fifteenMinutesAgo() &&
// don't display if it comes from me.
event.pubKey != signer.pubKey
) { // from the user
Log.d(TAG, "Notifying")
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.components
import android.view.View
import android.view.WindowManager
import android.widget.FrameLayout
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.window.DialogWindowProvider
@Composable
fun SetDialogToEdgeToEdge() {
val activityWindow = getActivityWindow()
val dialogWindow = (LocalView.current.parent as? DialogWindowProvider)?.window
val parentView = LocalView.current.parent as View
SideEffect {
if (activityWindow != null && dialogWindow != null) {
val attributes = WindowManager.LayoutParams()
attributes.copyFrom(activityWindow.attributes)
attributes.type = dialogWindow.attributes.type
dialogWindow.attributes = attributes
parentView.layoutParams =
FrameLayout.LayoutParams(
activityWindow.decorView.width,
activityWindow.decorView.height,
)
}
}
}
@@ -0,0 +1,54 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.components
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.view.Window
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.window.DialogWindowProvider
import com.vitorpamplona.amethyst.ui.navigation.getActivity
// Window utils
@Composable
fun getDialogWindow(): Window? = (LocalView.current.parent as? DialogWindowProvider)?.window
@Composable
fun getActivityWindow(): Window? = LocalView.current.context.getActivityWindow()
private tailrec fun Context.getActivityWindow(): Window? =
when (this) {
is Activity -> window
is ContextWrapper -> baseContext.getActivityWindow()
else -> null
}
@Composable
fun getActivity(): Activity? = LocalView.current.context.getActivity()
private tailrec fun Context.getActivity(): Activity? =
when (this) {
is Activity -> this
is ContextWrapper -> baseContext.getActivity()
else -> null
}
@@ -20,11 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.components
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}&quote={quote}&fork={fork}&version={version}&draft={draft}&enableMessageInterface={enableMessageInterface}",
icon = R.drawable.ic_moments,
arguments =
listOf(
navArgument("message") { type = NavType.StringType },
navArgument("attachment") { type = NavType.StringType },
navArgument("baseReplyTo") { type = NavType.StringType },
navArgument("quote") { type = NavType.StringType },
navArgument("fork") { type = NavType.StringType },
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,
)
}
}
}
@@ -48,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
@@ -81,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
@@ -105,21 +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
@@ -141,7 +142,7 @@ import com.vitorpamplona.amethyst.ui.theme.TinyBorders
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.mediumImportanceLink
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
@@ -348,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) {
@@ -380,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,
)
@@ -569,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) {
@@ -591,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(
@@ -620,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 }
@@ -1302,6 +1301,7 @@ private fun BoostTypeChoicePopup(
alignment = Alignment.BottomCenter,
offset = IntOffset(0, iconSizePx),
onDismissRequest = { visibilityState.targetState = false },
properties = PopupProperties(focusable = true),
) {
AnimatedVisibility(
visibleState = visibilityState,
@@ -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)
},
)
}
@@ -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,
)
}
@@ -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
@@ -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,
@@ -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(
@@ -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,
@@ -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,
@@ -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)
@@ -57,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
@@ -72,6 +71,7 @@ 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
@@ -393,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,
@@ -439,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)
@@ -178,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)
@@ -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
@@ -23,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 \"requests 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>
@@ -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
}
+5 -5
View File
@@ -1,6 +1,6 @@
[versions]
accompanistAdaptive = "0.34.0"
activityCompose = "1.9.2"
activityCompose = "1.9.3"
agp = "8.7.1"
android-compileSdk = "35"
android-minSdk = "26"
@@ -9,12 +9,12 @@ 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"
@@ -93,4 +93,21 @@ class Nip04Test {
assertTrue(Nip04.isNIP04("zJxfaJ32rN5Dg1ODjOlEew==?iv=EV5bUjcc4OX2Km/zPp4ndQ=="))
assertTrue(Nip04.isNIP04("6f8dMstm+udOu7yipSn33orTmwQpWbtfuY95NH+eTU1kArysWJIDkYgI2D25EAGIDJsNd45jOJ2NbVOhFiL3ZP/NWsTwXokk34iyHyA/lkjzugQ1bHXoMD1fP/Ay4hB4al1NHb8HXHKZaxPrErwdRDb8qa/I6dXb/1xxyVvNQBHHvmsM5yIFaPwnCN1DZqXf2KbTA/Ekz7Hy+7R+Sy3TXLQDFpWYqykppkXc7Fs0qSuPRyxz5+anuN0dxZa9GTwTEnBrZPbthKkNRrvZMdTGJ6WumOh9aUq8OJJWy9aOgsXvs7qjN1UqcCqQqYaVnEOhCaqWNDsVtsFrVDj+SaLIBvCiomwF4C4nIgngJ5I69tx0UNI0q+ZnvOGQZ7m1PpW2NYP7Yw43HJNdeUEQAmdCPnh/PJwzLTnIxHmQU7n7SPlMdV0SFa6H8y2HHvex697GAkyE5t8c2uO24OnqIwF1tR3blIqXzTSRl0GA6QvrSj2p4UtnWjvF7xT7RiIEyTtgU/AsihTrXyXzWWZaIBJogpgw6erlZqWjCH7sZy/WoGYEiblobOAqMYxax6vRbeuGtoYksr/myX+x9rfLrYuoDRTw4woXOLmMrrj+Mf0TbAgc3SjdkqdsPU1553rlSqIEZXuFgoWmxvVQDtekgTYyS97G81TDSK9nTJT5ilku8NVq2LgtBXGwsNIw/xekcOUzJke3kpnFPutNaexR1VF3ohIuqRKYRGcd8ADJP2lfwMcaGRiplAmFoaVS1YUhQwYFNq9rMLf7YauRGV4BJg/t9srdGxf5RoKCvRo+XM/nLxxysTR9MVaEP/3lDqjwChMxs+eWfLHE5vRWV8hUEqdrWNZV29gsx5nQpzJ4PARGZVu310pQzc6JAlc2XAhhFk6RamkYJnmCSMnb/RblzIATBi2kNrCVAlaXIon188inB62rEpZGPkRIP7PUfu27S/elLQHBHeGDsxOXsBRo1gl3te+raoBHsxo6zvRnYbwdAQa5taDE63eh+fT6kFI+xYmXNAQkU8Dp0MVhEh4JQI06Ni/AKrvYpC95TXXIphZcF+/Pv/vaGkhG2X9S3uhugwWK?iv=2vWkOQQi0WynNJz/aZ4k2g=="))
}
@Test
fun isNIP04EncodeWithBug() {
assertTrue(
Nip04.isNIP04(
"QOAYBWa88ConWs2C4kSvNqAcowCtg0ZRtAl7FyLSv9VMaJH4oCiDx0h8VLBnV97HdE4lv" +
"TW7AYC1eEw8/t1dbe0qRc3XrOt7MrPAO8yqpy1/3lFB1+10kip0+KdgT8Quvv02wTP8Dqi" +
"xpr2fliAIG2ONvDn+O5V0q9aVUN9HitgL/myTyR0T42edmxWeZoMBEOKvJyO80FekSsgVL" +
"ASafA/T5z4xs8oG88pSe9wSbSsw0xNjJeh3xLRCLuEuA9KI8hQ1Ys9nEax2UlaB/IL3o77" +
"OwBL+rrdUbNHTxYifgygRhg3BaXMsXRFNJbqYeMaRaNbvHkLVAQV2jLY4P/cKHBjEcTC/f" +
"lrCc2NCYF34rOQUY5EJVnFzM8qYVw6xNupBHTS7WFx1r60cPjG19P/+yoiTZ6bPdHTU0X2" +
"t64ovF2YWUq6/iKAclMaZDhWfrKqf82e62oIff55WQw2bw8A/jtBQVCf66EtEJ2OSFxNaZ" +
"rO+A4oLkHDCnAV+6fYzwo89gPOvORcVvSvg55yGiBFUZx9EHS6kdH1SU80/Mbxe2oI=" +
"?iv=gxz9pUFJFZHuV+D+hgKEOw==-null",
),
)
}
}
@@ -153,14 +153,21 @@ class Nip04(
}
fun isNIP04(encoded: String): Boolean {
val l = encoded.length
// cleaning up some bug from some client.
val cleanedUp = encoded.removeSuffix("-null")
val l = cleanedUp.length
if (l < 28) return false
return encoded[l - 28] == '?' && encoded[l - 27] == 'i' && encoded[l - 26] == 'v' && encoded[l - 25] == '='
return cleanedUp[l - 28] == '?' &&
cleanedUp[l - 27] == 'i' &&
cleanedUp[l - 26] == 'v' &&
cleanedUp[l - 25] == '='
}
fun decodeFromNIP04(payload: String): EncryptedInfo? =
try {
val parts = payload.split("?iv=")
// cleaning up some bug from some client.
val parts = payload.removeSuffix("-null").split("?iv=")
EncryptedInfo(
ciphertext = Base64.getDecoder().decode(parts[0]),
nonce = Base64.getDecoder().decode(parts[1]),