Delete Relay Selection dialog from all new post screens.

This commit is contained in:
Vitor Pamplona
2025-07-08 11:28:30 -04:00
parent a05b2681ad
commit 296fa201f7
15 changed files with 185 additions and 617 deletions
@@ -92,6 +92,7 @@ import com.vitorpamplona.quartz.experimental.profileGallery.dimension
import com.vitorpamplona.quartz.experimental.profileGallery.fromEvent import com.vitorpamplona.quartz.experimental.profileGallery.fromEvent
import com.vitorpamplona.quartz.experimental.profileGallery.hash import com.vitorpamplona.quartz.experimental.profileGallery.hash
import com.vitorpamplona.quartz.experimental.profileGallery.mimeType import com.vitorpamplona.quartz.experimental.profileGallery.mimeType
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
@@ -1085,14 +1086,15 @@ class Account(
fun consumeAndSendNip95( fun consumeAndSendNip95(
data: FileStorageEvent, data: FileStorageEvent,
signedEvent: FileStorageHeaderEvent, signedEvent: FileStorageHeaderEvent,
relayList: List<NormalizedRelayUrl>,
): Note? { ): Note? {
if (!isWriteable()) return null if (!isWriteable()) return null
client.send(data, relayList = relayList.toSet()) val relayList = computeRelayListToBroadcast(signedEvent)
client.send(data, relayList = relayList)
cache.justConsumeMyOwnEvent(data) cache.justConsumeMyOwnEvent(data)
client.send(signedEvent, relayList = relayList.toSet()) client.send(signedEvent, relayList = relayList)
cache.justConsumeMyOwnEvent(signedEvent) cache.justConsumeMyOwnEvent(signedEvent)
return cache.getNoteIfExists(signedEvent.id) return cache.getNoteIfExists(signedEvent.id)
@@ -1161,7 +1163,6 @@ class Account(
urlHeaderInfo: Map<String, FileHeader>, urlHeaderInfo: Map<String, FileHeader>,
caption: String?, caption: String?,
contentWarningReason: String?, contentWarningReason: String?,
relayList: Set<NormalizedRelayUrl>,
onReady: (Note) -> Unit, onReady: (Note) -> Unit,
) { ) {
val iMetas = val iMetas =
@@ -1180,7 +1181,7 @@ class Account(
) )
} }
signer.sign( val template =
PictureEvent.build(iMetas, caption ?: "") { PictureEvent.build(iMetas, caption ?: "") {
caption?.let { caption?.let {
hashtags(findHashtags(it)) hashtags(findHashtags(it))
@@ -1192,9 +1193,10 @@ class Account(
// add geohashes // add geohashes
// add title // add title
contentWarningReason?.let { contentWarning(contentWarningReason) } contentWarningReason?.let { contentWarning(contentWarningReason) }
}, }
) {
sendHeader(it, relayList = relayList, onReady) signAndComputeBroadcast(template) { event ->
cache.getNoteIfExists(event.id)?.let { onReady(it) }
} }
} }
@@ -1205,7 +1207,6 @@ class Account(
alt: String?, alt: String?,
contentWarningReason: String?, contentWarningReason: String?,
originalHash: String? = null, originalHash: String? = null,
relayList: Set<NormalizedRelayUrl>,
onReady: (Note) -> Unit, onReady: (Note) -> Unit,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -1273,8 +1274,8 @@ class Account(
} }
} }
signer.sign(template) { signAndComputeBroadcast(template) { event ->
sendHeader(it, relayList = relayList, onReady) cache.getNoteIfExists(event.id)?.let { onReady(it) }
} }
} }
@@ -1305,6 +1306,30 @@ class Account(
} }
} }
fun <T : Event> signAndComputeBroadcast(
template: EventTemplate<T>,
broadcast: List<Event> = emptyList(),
onDone: (T) -> Unit = {},
) {
signer.sign(template) { event ->
cache.justConsumeMyOwnEvent(event)
val note =
if (event is AddressableEvent) {
cache.getOrCreateAddressableNote(event.address())
} else {
cache.getOrCreateNote(event.id)
}
val relayList = computeRelayListToBroadcast(note)
client.send(event, relayList)
broadcast.forEach { client.send(it, relayList) }
onDone(event)
}
}
fun <T : Event> signAndSend( fun <T : Event> signAndSend(
template: EventTemplate<T>, template: EventTemplate<T>,
relayList: Set<NormalizedRelayUrl>, relayList: Set<NormalizedRelayUrl>,
@@ -1581,7 +1606,7 @@ class Account(
originalNote: Note, originalNote: Note,
notify: HexKey?, notify: HexKey?,
summary: String? = null, summary: String? = null,
relayList: List<NormalizedRelayUrl>, broadcast: List<Event>,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -1593,9 +1618,14 @@ class Account(
notify = notify, notify = notify,
summary = summary, summary = summary,
signer = signer, signer = signer,
) { ) { event ->
cache.justConsumeMyOwnEvent(it) cache.justConsumeMyOwnEvent(event)
client.send(it, relayList = relayList.toSet()) val note = cache.getOrCreateNote(event.id)
val relayList = computeRelayListToBroadcast(note)
client.send(event, relayList = relayList)
broadcast.forEach { client.send(it, relayList) }
} }
} }
@@ -1715,7 +1745,7 @@ class Account(
)?.relays()?.ifEmpty { null }?.toSet() )?.relays()?.ifEmpty { null }?.toSet()
if (relayList != null) { if (relayList != null) {
client.send(signedEvent = wrap, relayList = relayList) client.send(event = wrap, relayList = relayList)
} else { } else {
val taggedUserInboxRelays = val taggedUserInboxRelays =
wrap.taggedUserIds().flatMapTo(mutableSetOf()) { pubkey -> wrap.taggedUserIds().flatMapTo(mutableSetOf()) { pubkey ->
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.actions
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -96,7 +95,6 @@ import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest
import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField
import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
@@ -108,7 +106,6 @@ import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.amethyst.ui.theme.subtleBorder import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -128,12 +125,6 @@ fun EditPostView(
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var showRelaysDialog by remember { mutableStateOf(false) }
var relayList =
remember {
accountViewModel.account.outboxRelays.flow.value
.toImmutableList()
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
postViewModel.load(edit, versionLookingAt, accountViewModel) postViewModel.load(edit, versionLookingAt, accountViewModel)
@@ -148,16 +139,6 @@ fun EditPostView(
decorFitsSystemWindows = false, decorFitsSystemWindows = false,
), ),
) { ) {
if (showRelaysDialog) {
RelaySelectionDialog(
preSelectedList = relayList,
onClose = { showRelaysDialog = false },
onPost = { relayList = it },
accountViewModel = accountViewModel,
nav = nav,
)
}
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -169,22 +150,9 @@ fun EditPostView(
) { ) {
Spacer(modifier = StdHorzSpacer) Spacer(modifier = StdHorzSpacer)
Box {
IconButton(
modifier = Modifier.align(Alignment.Center),
onClick = { showRelaysDialog = true },
) {
Icon(
painter = painterRes(R.drawable.relays, 2),
contentDescription = stringRes(id = R.string.relay_list_selector),
modifier = Modifier.height(25.dp),
tint = MaterialTheme.colorScheme.onBackground,
)
}
}
PostButton( PostButton(
onPost = { onPost = {
postViewModel.sendPost(relayList = relayList) postViewModel.sendPost()
scope.launch { scope.launch {
delay(100) delay(100)
onClose() onClose()
@@ -47,7 +47,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder
import com.vitorpamplona.quartz.nip94FileMetadata.alt import com.vitorpamplona.quartz.nip94FileMetadata.alt
@@ -120,18 +120,21 @@ open class EditPostViewModel : ViewModel() {
editedFromNote = edit editedFromNote = edit
} }
fun sendPost(relayList: List<NormalizedRelayUrl>) { fun sendPost() {
viewModelScope.launch(Dispatchers.IO) { innerSendPost(relayList) } viewModelScope.launch(Dispatchers.IO) { innerSendPost() }
} }
suspend fun innerSendPost(relayList: List<NormalizedRelayUrl>) { suspend fun innerSendPost() {
if (accountViewModel == null) { if (accountViewModel == null) {
cancel() cancel()
return return
} }
val extraNotesToBroadcast = mutableListOf<Event>()
nip95attachments.forEach { nip95attachments.forEach {
account?.sendNip95(it.first, it.second, relayList.toSet()) extraNotesToBroadcast.add(it.first)
extraNotesToBroadcast.add(it.second)
} }
val notify = val notify =
@@ -147,7 +150,7 @@ open class EditPostViewModel : ViewModel() {
originalNote = editedFromNote!!, originalNote = editedFromNote!!,
notify = notify, notify = notify,
summary = subject.text.ifBlank { null }, summary = subject.text.ifBlank { null },
relayList = relayList, extraNotesToBroadcast,
) )
cancel() cancel()
@@ -39,7 +39,6 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.joinAll import kotlinx.coroutines.joinAll
@@ -82,13 +81,11 @@ open class NewMediaModel : ViewModel() {
fun upload( fun upload(
context: Context, context: Context,
relayList: List<NormalizedRelayUrl>,
onSucess: () -> Unit, onSucess: () -> Unit,
onError: (String, String) -> Unit, onError: (String, String) -> Unit,
) { ) {
viewModelScope.launch { viewModelScope.launch {
val myAccount = account ?: return@launch val myAccount = account ?: return@launch
if (relayList.isEmpty()) return@launch
val serverToUse = selectedServer ?: return@launch val serverToUse = selectedServer ?: return@launch
val myMultiOrchestrator = multiOrchestrator ?: return@launch val myMultiOrchestrator = multiOrchestrator ?: return@launch
@@ -141,7 +138,7 @@ open class NewMediaModel : ViewModel() {
withTimeoutOrNull(30000) { withTimeoutOrNull(30000) {
suspendCancellableCoroutine { continuation -> suspendCancellableCoroutine { continuation ->
account?.createNip95(it.bytes, headerInfo = it.fileHeader, caption, if (sensitiveContent) "" else null) { nip95 -> account?.createNip95(it.bytes, headerInfo = it.fileHeader, caption, if (sensitiveContent) "" else null) { nip95 ->
account?.consumeAndSendNip95(nip95.first, nip95.second, relayList) account?.consumeAndSendNip95(nip95.first, nip95.second)
continuation.resume(true) continuation.resume(true)
} }
} }
@@ -156,13 +153,12 @@ open class NewMediaModel : ViewModel() {
withTimeoutOrNull(30000) { withTimeoutOrNull(30000) {
suspendCancellableCoroutine { continuation -> suspendCancellableCoroutine { continuation ->
account?.sendHeader( account?.sendHeader(
it.url, url = it.url,
it.magnet, magnetUri = it.magnet,
it.fileHeader, headerInfo = it.fileHeader,
caption, alt = caption,
if (sensitiveContent) "" else null, contentWarningReason = if (sensitiveContent) "" else null,
it.uploadedHash, originalHash = it.uploadedHash,
relayList.toSet(),
) { ) {
continuation.resume(true) continuation.resume(true)
} }
@@ -178,10 +174,9 @@ open class NewMediaModel : ViewModel() {
withTimeoutOrNull(30000) { withTimeoutOrNull(30000) {
suspendCancellableCoroutine { continuation -> suspendCancellableCoroutine { continuation ->
account?.sendAllAsOnePictureEvent( account?.sendAllAsOnePictureEvent(
imageUrls, urlHeaderInfo = imageUrls,
caption, caption = caption,
if (sensitiveContent) "" else null, contentWarningReason = if (sensitiveContent) "" else null,
relayList.toSet(),
) { ) {
continuation.resume(true) continuation.resume(true)
} }
@@ -35,8 +35,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@@ -49,7 +47,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -70,7 +67,6 @@ import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton
import com.vitorpamplona.amethyst.ui.note.buttons.PostButton import com.vitorpamplona.amethyst.ui.note.buttons.PostButton
import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.SettingSwitchItem import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.SettingSwitchItem
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
@@ -100,13 +96,6 @@ fun NewMediaView(
postViewModel.load(account, uris) postViewModel.load(account, uris)
} }
var showRelaysDialog by remember { mutableStateOf(false) }
var relayList =
remember {
accountViewModel.account.outboxRelays.flow.value
.toImmutableList()
}
Dialog( Dialog(
onDismissRequest = { onClose() }, onDismissRequest = { onClose() },
properties = properties =
@@ -128,23 +117,9 @@ fun NewMediaView(
) { ) {
Spacer(modifier = StdHorzSpacer) Spacer(modifier = StdHorzSpacer)
Box {
IconButton(
modifier = Modifier.align(Alignment.Center),
onClick = { showRelaysDialog = true },
) {
Icon(
painter = painterRes(R.drawable.relays, 3),
contentDescription = stringRes(id = R.string.relay_list_selector),
modifier = Modifier.height(25.dp),
tint = MaterialTheme.colorScheme.onBackground,
)
}
}
PostButton( PostButton(
onPost = { onPost = {
postViewModel.upload(context, relayList, onClose, accountViewModel.toastManager::toast) postViewModel.upload(context, onClose, accountViewModel.toastManager::toast)
postViewModel.selectedServer?.let { postViewModel.selectedServer?.let {
if (it.type != ServerType.NIP95) { if (it.type != ServerType.NIP95) {
account.settings.changeDefaultFileServer(it) account.settings.changeDefaultFileServer(it)
@@ -173,16 +148,6 @@ fun NewMediaView(
) )
}, },
) { pad -> ) { pad ->
if (showRelaysDialog) {
RelaySelectionDialog(
preSelectedList = relayList,
onClose = { showRelaysDialog = false },
onPost = { relayList = it },
accountViewModel = accountViewModel,
nav = nav,
)
}
Surface( Surface(
modifier = modifier =
Modifier Modifier
@@ -66,7 +66,7 @@ fun NewPollVoteValueRange(pollViewModel: ShortNotePostViewModel) {
onValueChange = { pollViewModel.updateMinZapAmountForPoll(it) }, onValueChange = { pollViewModel.updateMinZapAmountForPoll(it) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
colors = if (pollViewModel.isValidvalueMinimum.value) colorValid else colorInValid, colors = if (pollViewModel.isValidValueMinimum.value) colorValid else colorInValid,
label = { label = {
Text( Text(
text = stringRes(R.string.poll_zap_value_min), text = stringRes(R.string.poll_zap_value_min),
@@ -88,7 +88,7 @@ fun NewPollVoteValueRange(pollViewModel: ShortNotePostViewModel) {
onValueChange = { pollViewModel.updateMaxZapAmountForPoll(it) }, onValueChange = { pollViewModel.updateMaxZapAmountForPoll(it) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
colors = if (pollViewModel.isValidvalueMaximum.value) colorValid else colorInValid, colors = if (pollViewModel.isValidValueMaximum.value) colorValid else colorInValid,
label = { label = {
Text( Text(
text = stringRes(R.string.poll_zap_value_max), text = stringRes(R.string.poll_zap_value_max),
@@ -1,278 +0,0 @@
/**
* Copyright (c) 2025 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.actions
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton
import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationDialog
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlin.collections.map
data class RelayList(
val relay: NormalizedRelayUrl,
val isSelected: Boolean,
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RelaySelectionDialog(
preSelectedList: ImmutableList<NormalizedRelayUrl>,
onClose: () -> Unit,
onPost: (list: ImmutableList<NormalizedRelayUrl>) -> Unit,
accountViewModel: AccountViewModel,
nav: INav,
) {
var relays by remember {
mutableStateOf(
accountViewModel.account.client.allAvailableRelays().map {
RelayList(
relay = it,
isSelected = preSelectedList.any { relayUrl -> it == relayUrl },
)
},
)
}
val hasSelectedRelay by remember { derivedStateOf { relays.any { it.isSelected } } }
var selected by remember { mutableStateOf(true) }
Dialog(
onDismissRequest = { onClose() },
properties =
DialogProperties(
usePlatformDefaultWidth = false,
dismissOnClickOutside = false,
decorFitsSystemWindows = false,
),
) {
SetDialogToEdgeToEdge()
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
actions = {
SaveButton(
onPost = {
val selectedRelays = relays.filter { it.isSelected }.map { it.relay }.toImmutableList()
onPost(selectedRelays)
onClose()
},
isActive = hasSelectedRelay,
)
Spacer(modifier = StdHorzSpacer)
},
title = {},
navigationIcon = {
Row {
Spacer(modifier = StdHorzSpacer)
CloseButton(
onPress = { onClose() },
)
}
},
)
},
) { pad ->
Column(
modifier =
Modifier.padding(pad).padding(start = 10.dp, end = 10.dp, top = 10.dp),
) {
RelaySwitch(
text = stringRes(R.string.select_deselect_all),
checked = selected,
onClick = {
selected = !selected
relays = relays.mapIndexed { _, item -> item.copy(isSelected = selected) }
},
)
LazyColumn(
contentPadding = FeedPadding,
) {
itemsIndexed(
relays,
key = { _, item -> item.relay.url },
) { index, item ->
RenderRelaySwitch(
item,
onClick = {
relays =
relays.mapIndexed { j, item ->
if (index == j) {
item.copy(isSelected = !item.isSelected)
} else {
item
}
}
},
accountViewModel,
nav,
)
}
}
}
}
}
}
@Composable
fun RenderRelaySwitch(
item: RelayList,
onClick: () -> Unit,
accountViewModel: AccountViewModel,
nav: INav,
) {
val context = LocalContext.current
val relayInfo by loadRelayInfo(item.relay, accountViewModel)
var openRelayDialog by remember { mutableStateOf(false) }
if (openRelayDialog) {
RelayInformationDialog(
onClose = { openRelayDialog = false },
relayInfo = relayInfo,
relay = item.relay,
accountViewModel = accountViewModel,
nav = nav,
)
}
RelaySwitch(
text = item.relay.displayUrl(),
checked = item.isSelected,
onClick = onClick,
onLongPress = {
openRelayDialog = true
accountViewModel.retrieveRelayDocument(
relay = item.relay,
onInfo = { },
onError = { relay, errorCode, exceptionMessage ->
val msg =
when (errorCode) {
Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL ->
stringRes(
context,
R.string.relay_information_document_error_assemble_url,
relay.url,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER ->
stringRes(
context,
R.string.relay_information_document_error_assemble_url,
relay.url,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT ->
stringRes(
context,
R.string.relay_information_document_error_assemble_url,
relay.url,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS ->
stringRes(
context,
R.string.relay_information_document_error_assemble_url,
relay.url,
exceptionMessage,
)
}
accountViewModel.toastManager.toast(
stringRes(context, R.string.unable_to_download_relay_document),
msg,
)
},
)
},
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RelaySwitch(
text: String,
checked: Boolean,
onClick: () -> Unit,
onLongPress: () -> Unit = {},
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier.combinedClickable(
onClick = onClick,
onLongClick = onLongPress,
),
) {
Text(
modifier = Modifier.weight(1f),
text = text,
)
Switch(
checked = checked,
onCheckedChange = { onClick() },
)
}
}
@@ -64,7 +64,6 @@ import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash
import com.vitorpamplona.quartz.nip01Core.tags.geohash.hasGeohashes import com.vitorpamplona.quartz.nip01Core.tags.geohash.hasGeohashes
@@ -99,13 +98,14 @@ import com.vitorpamplona.quartz.nip94FileMetadata.mimeType
import com.vitorpamplona.quartz.nip94FileMetadata.originalHash import com.vitorpamplona.quartz.nip94FileMetadata.originalHash
import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent
import com.vitorpamplona.quartz.nip94FileMetadata.size import com.vitorpamplona.quartz.nip94FileMetadata.size
import com.vitorpamplona.quartz.utils.tryAndWait
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.collections.plus import kotlin.collections.plus
import kotlin.coroutines.resume
@Stable @Stable
open class CommentPostViewModel : open class CommentPostViewModel :
@@ -131,7 +131,7 @@ open class CommentPostViewModel :
var account: Account? = null var account: Account? = null
var externalIdentity by mutableStateOf<ExternalId?>(null) var externalIdentity by mutableStateOf<ExternalId?>(null)
var replyingTo: Note? by mutableStateOf<Note?>(null) var replyingTo: Note? by mutableStateOf(null)
val iMetaAttachments = IMetaAttachments() val iMetaAttachments = IMetaAttachments()
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>( var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(
@@ -177,9 +177,6 @@ open class CommentPostViewModel :
var wantsZapraiser by mutableStateOf(false) var wantsZapraiser by mutableStateOf(false)
override val zapRaiserAmount = mutableStateOf<Long?>(null) override val zapRaiserAmount = mutableStateOf<Long?>(null)
var showRelaysDialog by mutableStateOf(false)
var relayList by mutableStateOf<ImmutableList<NormalizedRelayUrl>?>(null)
fun lnAddress(): String? = account?.userProfile()?.info?.lnAddress() fun lnAddress(): String? = account?.userProfile()?.info?.lnAddress()
fun hasLnAddress(): Boolean = account?.userProfile()?.info?.lnAddress() != null fun hasLnAddress(): Boolean = account?.userProfile()?.info?.lnAddress() != null
@@ -270,15 +267,15 @@ open class CommentPostViewModel :
canAddZapRaiser = accountViewModel?.userProfile()?.info?.lnAddress() != null canAddZapRaiser = accountViewModel?.userProfile()?.info?.lnAddress() != null
multiOrchestrator = null multiOrchestrator = null
val localfowardZapTo = draftEvent.tags.filter { it.size > 1 && it[0] == "zap" } val localForwardZapTo = draftEvent.tags.filter { it.size > 1 && it[0] == "zap" }
forwardZapTo.value = SplitBuilder() forwardZapTo.value = SplitBuilder()
localfowardZapTo.forEach { localForwardZapTo.forEach {
val user = LocalCache.getOrCreateUser(it[1]) val user = LocalCache.getOrCreateUser(it[1])
val value = it.last().toFloatOrNull() ?: 0f val value = it.last().toFloatOrNull() ?: 0f
forwardZapTo.value.addItem(user, value) forwardZapTo.value.addItem(user, value)
} }
forwardZapToEditting.value = TextFieldValue("") forwardZapToEditting.value = TextFieldValue("")
wantsForwardZapTo = localfowardZapTo.isNotEmpty() wantsForwardZapTo = localForwardZapTo.isNotEmpty()
wantsToMarkAsSensitive = draftEvent.isSensitive() wantsToMarkAsSensitive = draftEvent.isSensitive()
@@ -311,21 +308,23 @@ open class CommentPostViewModel :
suspend fun sendPostSync() { suspend fun sendPostSync() {
val template = createTemplate() ?: return val template = createTemplate() ?: return
val relayList = relayList val extraNotesToBroadcast = mutableListOf<Event>()
if (nip95attachments.isNotEmpty() && relayList != null && relayList.isNotEmpty()) { if (nip95attachments.isNotEmpty()) {
val usedImages = template.tags.taggedQuoteIds().toSet() val usedImages = template.tags.taggedQuoteIds().toSet()
nip95attachments.forEach { nip95attachments.forEach {
if (usedImages.contains(it.second.id)) { if (usedImages.contains(it.second.id)) {
account?.sendNip95(it.first, it.second, relayList.toSet()) extraNotesToBroadcast.add(it.first)
extraNotesToBroadcast.add(it.second)
} }
} }
} }
accountViewModel?.account?.signAndSendPrivatelyOrBroadcast( tryAndWait { continuation ->
template, accountViewModel?.account?.signAndComputeBroadcast(template, extraNotesToBroadcast) {
relayList = { relayList }, continuation.resume(it)
) }
}
accountViewModel?.deleteDraft(draftTag.current) accountViewModel?.deleteDraft(draftTag.current)
@@ -378,7 +377,7 @@ open class CommentPostViewModel :
msg = tagger.message, msg = tagger.message,
replyingTo = eventHint, replyingTo = eventHint,
) { ) {
tagger.pTags?.let { notify(it.map { it.toPTag() }) } tagger.pTags?.let { pTagList -> notify(pTagList.map { it.toPTag() }) }
hashtags(findHashtags(tagger.message)) hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message)) references(findURLs(tagger.message))
@@ -398,7 +397,7 @@ open class CommentPostViewModel :
msg = tagger.message, msg = tagger.message,
extId = externalIdentity, extId = externalIdentity,
) { ) {
tagger.pTags?.let { notify(it.map { it.toPTag() }) } tagger.pTags?.let { pTagList -> notify(pTagList.map { it.toPTag() }) }
hashtags(findHashtags(tagger.message)) hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message)) references(findURLs(tagger.message))
@@ -458,9 +457,9 @@ open class CommentPostViewModel :
) )
if (results.allGood) { if (results.allGood) {
results.successful.forEach { results.successful.forEach { state ->
if (it.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) {
account?.createNip95(it.result.bytes, headerInfo = it.result.fileHeader, alt, contentWarningReason) { nip95 -> account?.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason) { nip95 ->
nip95attachments = nip95attachments + nip95 nip95attachments = nip95attachments + nip95
val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) } val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) }
@@ -469,20 +468,20 @@ open class CommentPostViewModel :
urlPreviews.update(message) urlPreviews.update(message)
} }
} }
} else if (it.result is UploadOrchestrator.OrchestratorResult.ServerResult) { } else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
val iMeta = val iMeta =
IMetaTagBuilder(it.result.url) IMetaTagBuilder(state.result.url)
.apply { .apply {
hash(it.result.fileHeader.hash) hash(state.result.fileHeader.hash)
size(it.result.fileHeader.size) size(state.result.fileHeader.size)
it.result.fileHeader.mimeType state.result.fileHeader.mimeType
?.let { mimeType(it) } ?.let { mimeType(it) }
it.result.fileHeader.dim state.result.fileHeader.dim
?.let { dims(it) } ?.let { dims(it) }
it.result.fileHeader.blurHash state.result.fileHeader.blurHash
?.let { blurhash(it.blurhash) } ?.let { blurhash(it.blurhash) }
it.result.magnet?.let { magnet(it) } state.result.magnet?.let { magnet(it) }
it.result.uploadedHash?.let { originalHash(it) } state.result.uploadedHash?.let { originalHash(it) }
alt?.let { alt(it) } alt?.let { alt(it) }
contentWarningReason?.let { sensitiveContent(contentWarningReason) } contentWarningReason?.let { sensitiveContent(contentWarningReason) }
@@ -490,7 +489,7 @@ open class CommentPostViewModel :
iMetaAttachments.replace(iMeta.url, iMeta) iMetaAttachments.replace(iMeta.url, iMeta)
message = message.insertUrlAtCursor(it.result.url) message = message.insertUrlAtCursor(state.result.url)
urlPreviews.update(message) urlPreviews.update(message)
} }
} }
@@ -546,21 +545,9 @@ open class CommentPostViewModel :
emojiSuggestions?.reset() emojiSuggestions?.reset()
showRelaysDialog = false
reloadRelaySet()
draftTag.rotate() draftTag.rotate()
} }
fun reloadRelaySet() {
val account = accountViewModel?.account ?: return
relayList =
account.outboxRelays.flow.value
.toImmutableList()
}
fun deleteMediaToUpload(selected: SelectedMediaProcessing) { fun deleteMediaToUpload(selected: SelectedMediaProcessing) {
this.multiOrchestrator?.remove(selected) this.multiOrchestrator?.remove(selected)
} }
@@ -569,8 +556,8 @@ open class CommentPostViewModel :
notifying = notifying?.filter { it != userToRemove } notifying = notifying?.filter { it != userToRemove }
} }
override fun updateMessage(it: TextFieldValue) { override fun updateMessage(newMessage: TextFieldValue) {
message = it message = newMessage
urlPreviews.update(message) urlPreviews.update(message)
if (message.selection.collapsed) { if (message.selection.collapsed) {
@@ -627,13 +614,13 @@ open class CommentPostViewModel :
val wordToInsert = item.link.url + " " val wordToInsert = item.link.url + " "
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
iMetaAttachments.downloadAndPrepare( iMetaAttachments.downloadAndPrepare(item.link.url) {
item.link.url, Amethyst.Companion.instance.okHttpClients
{ .getHttpClient(
Amethyst.Companion.instance.okHttpClients accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url)
.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) ?: false,
}, )
) }
} }
message = message.replaceCurrentWord(wordToInsert) message = message.replaceCurrentWord(wordToInsert)
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.note.nip22Comments
import android.net.Uri import android.net.Uri
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -37,8 +36,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@@ -46,7 +43,6 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -56,7 +52,6 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.RelaySelectionDialog
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
@@ -85,7 +80,6 @@ import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.AddZapraiserButton
import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.ZapRaiserRequest import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.ZapRaiserRequest
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapToButton import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapToButton
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size10dp
@@ -116,7 +110,6 @@ fun ReplyCommentPostScreen(
val context = LocalContext.current val context = LocalContext.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
postViewModel.reloadRelaySet()
reply?.let { reply?.let {
postViewModel.reply(it) postViewModel.reply(it)
} }
@@ -159,20 +152,6 @@ fun GenericCommentPostScreen(
verticalAlignment = CenterVertically, verticalAlignment = CenterVertically,
) { ) {
Spacer(modifier = StdHorzSpacer) Spacer(modifier = StdHorzSpacer)
Box {
IconButton(
modifier = Modifier.align(Alignment.Center),
onClick = { postViewModel.showRelaysDialog = true },
) {
Icon(
painter = painterRes(R.drawable.relays, 5),
contentDescription = stringRes(id = R.string.relay_list_selector),
modifier = Modifier.height(25.dp),
tint = MaterialTheme.colorScheme.onBackground,
)
}
}
PostButton( PostButton(
onPost = { onPost = {
// uses the accountViewModel scope to avoid cancelling this // uses the accountViewModel scope to avoid cancelling this
@@ -209,15 +188,6 @@ fun GenericCommentPostScreen(
) )
}, },
) { pad -> ) { pad ->
if (postViewModel.showRelaysDialog) {
RelaySelectionDialog(
preSelectedList = postViewModel.relayList ?: persistentListOf(),
onClose = { postViewModel.showRelaysDialog = false },
onPost = { postViewModel.relayList = it },
accountViewModel = accountViewModel,
nav = nav,
)
}
Surface( Surface(
modifier = modifier =
Modifier Modifier
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds
import android.net.Uri import android.net.Uri
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -37,8 +36,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@@ -47,10 +44,7 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -60,7 +54,6 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.RelaySelectionDialog
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
@@ -88,7 +81,6 @@ import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.AddZapraiserButton
import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.ZapRaiserRequest import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.ZapRaiserRequest
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapToButton import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapToButton
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size10dp
@@ -151,8 +143,6 @@ fun NewProductScreen(
) { ) {
WatchAndLoadMyEmojiList(accountViewModel) WatchAndLoadMyEmojiList(accountViewModel)
var showRelaysDialog by remember { mutableStateOf(false) }
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -164,19 +154,6 @@ fun NewProductScreen(
) { ) {
Spacer(modifier = StdHorzSpacer) Spacer(modifier = StdHorzSpacer)
Box {
IconButton(
modifier = Modifier.align(Alignment.Center),
onClick = { showRelaysDialog = true },
) {
Icon(
painter = painterRes(R.drawable.relays, 1),
contentDescription = stringRes(id = R.string.relay_list_selector),
modifier = Modifier.height(25.dp),
tint = MaterialTheme.colorScheme.onBackground,
)
}
}
PostButton( PostButton(
onPost = { onPost = {
accountViewModel.viewModelScope.launch(Dispatchers.IO) { accountViewModel.viewModelScope.launch(Dispatchers.IO) {
@@ -210,15 +187,6 @@ fun NewProductScreen(
) )
}, },
) { pad -> ) { pad ->
if (showRelaysDialog) {
RelaySelectionDialog(
preSelectedList = postViewModel.relayList ?: persistentListOf(),
onClose = { showRelaysDialog = false },
onPost = { postViewModel.relayList = it },
accountViewModel = accountViewModel,
nav = nav,
)
}
Surface( Surface(
modifier = modifier =
Modifier Modifier
@@ -54,7 +54,6 @@ fun GeoHashPostScreen(
val context = LocalContext.current val context = LocalContext.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
postViewModel.reloadRelaySet()
geohash?.let { geohash?.let {
postViewModel.newPostFor(GeohashId(it)) postViewModel.newPostFor(GeohashId(it))
} }
@@ -54,7 +54,6 @@ fun HashtagPostScreen(
val context = LocalContext.current val context = LocalContext.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
postViewModel.reloadRelaySet()
hashtag?.let { hashtag?.let {
postViewModel.newPostFor(HashtagId(it)) postViewModel.newPostFor(HashtagId(it))
} }
@@ -25,7 +25,6 @@ import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -50,8 +49,6 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -62,7 +59,6 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.RelaySelectionDialog
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
@@ -173,8 +169,6 @@ private fun NewPostScreenInner(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: Nav, nav: Nav,
) { ) {
val scope = rememberCoroutineScope()
WatchAndLoadMyEmojiList(accountViewModel) WatchAndLoadMyEmojiList(accountViewModel)
Scaffold( Scaffold(
@@ -188,19 +182,6 @@ private fun NewPostScreenInner(
) { ) {
Spacer(modifier = StdHorzSpacer) Spacer(modifier = StdHorzSpacer)
Box {
IconButton(
modifier = Modifier.align(Alignment.Center),
onClick = { postViewModel.showRelaysDialog = true },
) {
Icon(
painter = painterRes(R.drawable.relays, 6),
contentDescription = stringRes(id = R.string.relay_list_selector),
modifier = Modifier.height(25.dp),
tint = MaterialTheme.colorScheme.onBackground,
)
}
}
PostButton( PostButton(
onPost = { onPost = {
// uses the accountViewModel scope to avoid cancelling this // uses the accountViewModel scope to avoid cancelling this
@@ -238,15 +219,6 @@ private fun NewPostScreenInner(
) )
}, },
) { pad -> ) { pad ->
if (postViewModel.showRelaysDialog) {
RelaySelectionDialog(
preSelectedList = postViewModel.relayList ?: persistentListOf(),
onClose = { postViewModel.showRelaysDialog = false },
onPost = { postViewModel.relayList = it },
accountViewModel = accountViewModel,
nav = nav,
)
}
Surface( Surface(
modifier = modifier =
Modifier Modifier
@@ -419,7 +391,7 @@ private fun NewPostScreenBody(
} }
} }
if (postViewModel.wantsZapraiser && postViewModel.hasLnAddress()) { if (postViewModel.wantsZapRaiser && postViewModel.hasLnAddress()) {
Row( Row(
verticalAlignment = CenterVertically, verticalAlignment = CenterVertically,
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp), modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
@@ -495,8 +467,8 @@ private fun BottomRowActions(postViewModel: ShortNotePostViewModel) {
} }
if (postViewModel.canAddZapRaiser) { if (postViewModel.canAddZapRaiser) {
AddZapraiserButton(postViewModel.wantsZapraiser) { AddZapraiserButton(postViewModel.wantsZapRaiser) {
postViewModel.wantsZapraiser = !postViewModel.wantsZapraiser postViewModel.wantsZapRaiser = !postViewModel.wantsZapRaiser
} }
} }
@@ -74,7 +74,6 @@ import com.vitorpamplona.quartz.experimental.zapPolls.tags.PollOptionTag
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash
import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash
@@ -115,12 +114,13 @@ import com.vitorpamplona.quartz.nip94FileMetadata.mimeType
import com.vitorpamplona.quartz.nip94FileMetadata.originalHash import com.vitorpamplona.quartz.nip94FileMetadata.originalHash
import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent
import com.vitorpamplona.quartz.nip94FileMetadata.size import com.vitorpamplona.quartz.nip94FileMetadata.size
import com.vitorpamplona.quartz.utils.tryAndWait
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.coroutines.resume
enum class UserSuggestionAnchor { enum class UserSuggestionAnchor {
MAIN_MESSAGE, MAIN_MESSAGE,
@@ -151,8 +151,8 @@ open class ShortNotePostViewModel :
var accountViewModel: AccountViewModel? = null var accountViewModel: AccountViewModel? = null
var account: Account? = null var account: Account? = null
var originalNote: Note? by mutableStateOf<Note?>(null) var originalNote: Note? by mutableStateOf(null)
var forkedFromNote: Note? by mutableStateOf<Note?>(null) var forkedFromNote: Note? by mutableStateOf(null)
var pTags by mutableStateOf<List<User>?>(null) var pTags by mutableStateOf<List<User>?>(null)
var eTags by mutableStateOf<List<Note>?>(null) var eTags by mutableStateOf<List<Note>?>(null)
@@ -185,9 +185,8 @@ open class ShortNotePostViewModel :
var consensusThreshold: Int? = null var consensusThreshold: Int? = null
var closedAt: Long? = null var closedAt: Long? = null
var isValidRecipients = mutableStateOf(true) var isValidValueMaximum = mutableStateOf(true)
var isValidvalueMaximum = mutableStateOf(true) var isValidValueMinimum = mutableStateOf(true)
var isValidvalueMinimum = mutableStateOf(true)
var isValidConsensusThreshold = mutableStateOf(true) var isValidConsensusThreshold = mutableStateOf(true)
var isValidClosedAt = mutableStateOf(true) var isValidClosedAt = mutableStateOf(true)
@@ -212,12 +211,9 @@ open class ShortNotePostViewModel :
// ZapRaiser // ZapRaiser
var canAddZapRaiser by mutableStateOf(false) var canAddZapRaiser by mutableStateOf(false)
var wantsZapraiser by mutableStateOf(false) var wantsZapRaiser by mutableStateOf(false)
override val zapRaiserAmount = mutableStateOf<Long?>(null) override val zapRaiserAmount = mutableStateOf<Long?>(null)
var showRelaysDialog by mutableStateOf(false)
var relayList by mutableStateOf<ImmutableList<NormalizedRelayUrl>?>(null)
fun lnAddress(): String? = account?.userProfile()?.info?.lnAddress() fun lnAddress(): String? = account?.userProfile()?.info?.lnAddress()
fun hasLnAddress(): Boolean = account?.userProfile()?.info?.lnAddress() != null fun hasLnAddress(): Boolean = account?.userProfile()?.info?.lnAddress() != null
@@ -295,10 +291,10 @@ open class ShortNotePostViewModel :
canUsePoll = originalNote == null canUsePoll = originalNote == null
multiOrchestrator = null multiOrchestrator = null
quote?.let { quote?.let { quotedNote ->
message = TextFieldValue(message.text + "\nnostr:${it.toNEvent()}") message = TextFieldValue(message.text + "\nnostr:${quotedNote.toNEvent()}")
it.author?.let { quotedUser -> quotedNote.author?.let { quotedUser ->
if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) { if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
if (forwardZapTo.value.items.none { it.key.pubkeyHex == quotedUser.pubkeyHex }) { if (forwardZapTo.value.items.none { it.key.pubkeyHex == quotedUser.pubkeyHex }) {
forwardZapTo.value.addItem(quotedUser) forwardZapTo.value.addItem(quotedUser)
@@ -313,21 +309,21 @@ open class ShortNotePostViewModel :
} }
} }
fork?.let { fork?.let { forkedNoted ->
message = TextFieldValue(version?.event?.content ?: it.event?.content ?: "") message = TextFieldValue(version?.event?.content ?: forkedNoted.event?.content ?: "")
it.event?.isSensitiveOrNSFW()?.let { forkedNoted.event?.isSensitiveOrNSFW()?.let {
if (it) wantsToMarkAsSensitive = true if (it) wantsToMarkAsSensitive = true
} }
it.event?.zapraiserAmount()?.let { forkedNoted.event?.zapraiserAmount()?.let {
zapRaiserAmount.value = it zapRaiserAmount.value = it
} }
it.event?.zapSplitSetup()?.let { forkedNoted.event?.zapSplitSetup()?.let { setup ->
val totalWeight = it.sumOf { if (it is ZapSplitSetupLnAddress) 0.0 else it.weight } val totalWeight = setup.sumOf { if (it is ZapSplitSetupLnAddress) 0.0 else it.weight }
it.forEach { setup.forEach {
if (it is ZapSplitSetup) { if (it is ZapSplitSetup) {
forwardZapTo.value.addItem(LocalCache.getOrCreateUser(it.pubKeyHex), (it.weight / totalWeight).toFloat()) forwardZapTo.value.addItem(LocalCache.getOrCreateUser(it.pubKeyHex), (it.weight / totalWeight).toFloat())
} }
@@ -336,7 +332,7 @@ open class ShortNotePostViewModel :
// Only adds if it is not already set up. // Only adds if it is not already set up.
if (forwardZapTo.value.items.isEmpty()) { if (forwardZapTo.value.items.isEmpty()) {
it.author?.let { forkedAuthor -> forkedNoted.author?.let { forkedAuthor ->
if (forkedAuthor.pubkeyHex != accountViewModel.userProfile().pubkeyHex) { if (forkedAuthor.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
if (forwardZapTo.value.items.none { it.key.pubkeyHex == forkedAuthor.pubkeyHex }) forwardZapTo.value.addItem(forkedAuthor) if (forwardZapTo.value.items.none { it.key.pubkeyHex == forkedAuthor.pubkeyHex }) forwardZapTo.value.addItem(forkedAuthor)
if (forwardZapTo.value.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) forwardZapTo.value.addItem(accountViewModel.userProfile()) if (forwardZapTo.value.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) forwardZapTo.value.addItem(accountViewModel.userProfile())
@@ -347,7 +343,7 @@ open class ShortNotePostViewModel :
} }
} }
it.author?.let { forkedNoted.author?.let {
if (this.pTags == null) { if (this.pTags == null) {
this.pTags = listOf(it) this.pTags = listOf(it)
} else if (this.pTags?.contains(it) != true) { } else if (this.pTags?.contains(it) != true) {
@@ -355,7 +351,7 @@ open class ShortNotePostViewModel :
} }
} }
forkedFromNote = it forkedFromNote = forkedNoted
} ?: run { } ?: run {
forkedFromNote = null forkedFromNote = null
} }
@@ -380,15 +376,15 @@ open class ShortNotePostViewModel :
canAddZapRaiser = accountViewModel?.userProfile()?.info?.lnAddress() != null canAddZapRaiser = accountViewModel?.userProfile()?.info?.lnAddress() != null
multiOrchestrator = null multiOrchestrator = null
val localfowardZapTo = draftEvent.tags.filter { it.size > 1 && it[0] == "zap" } val localForwardZapTo = draftEvent.tags.filter { it.size > 1 && it[0] == "zap" }
forwardZapTo.value = SplitBuilder() forwardZapTo.value = SplitBuilder()
localfowardZapTo.forEach { localForwardZapTo.forEach {
val user = LocalCache.getOrCreateUser(it[1]) val user = LocalCache.getOrCreateUser(it[1])
val value = it.last().toFloatOrNull() ?: 0f val value = it.last().toFloatOrNull() ?: 0f
forwardZapTo.value.addItem(user, value) forwardZapTo.value.addItem(user, value)
} }
forwardZapToEditting.value = TextFieldValue("") forwardZapToEditting.value = TextFieldValue("")
wantsForwardZapTo = localfowardZapTo.isNotEmpty() wantsForwardZapTo = localForwardZapTo.isNotEmpty()
wantsToMarkAsSensitive = draftEvent.isSensitive() wantsToMarkAsSensitive = draftEvent.isSensitive()
@@ -398,11 +394,11 @@ open class ShortNotePostViewModel :
wantsExclusiveGeoPost = draftEvent.kind == CommentEvent.KIND wantsExclusiveGeoPost = draftEvent.kind == CommentEvent.KIND
} }
val zapraiser = draftEvent.zapraiserAmount() val zapRaiser = draftEvent.zapraiserAmount()
wantsZapraiser = zapraiser != null wantsZapRaiser = zapRaiser != null
zapRaiserAmount.value = null zapRaiserAmount.value = null
if (zapraiser != null) { if (zapRaiser != null) {
zapRaiserAmount.value = zapraiser zapRaiserAmount.value = zapRaiser
} }
eTags = eTags =
@@ -416,7 +412,7 @@ open class ShortNotePostViewModel :
LocalCache.getOrCreateUser(it[1]) LocalCache.getOrCreateUser(it[1])
} }
draftEvent.tags.filter { it.size > 3 && (it[0] == "e" || it[0] == "a") && it.get(3) == "fork" }.forEach { draftEvent.tags.filter { it.size > 3 && (it[0] == "e" || it[0] == "a") && it[3] == "fork" }.forEach {
val note = LocalCache.checkGetOrCreateNote(it[1]) val note = LocalCache.checkGetOrCreateNote(it[1])
forkedFromNote = note forkedFromNote = note
} }
@@ -470,21 +466,23 @@ open class ShortNotePostViewModel :
suspend fun sendPostSync() { suspend fun sendPostSync() {
val template = createTemplate() ?: return val template = createTemplate() ?: return
val relayList = relayList val extraNotesToBroadcast = mutableListOf<Event>()
if (nip95attachments.isNotEmpty() && relayList != null && relayList.isNotEmpty()) { if (nip95attachments.isNotEmpty()) {
val usedImages = template.tags.taggedQuoteIds().toSet() val usedImages = template.tags.taggedQuoteIds().toSet()
nip95attachments.forEach { nip95attachments.forEach {
if (usedImages.contains(it.second.id)) { if (usedImages.contains(it.second.id)) {
account?.sendNip95(it.first, it.second, relayList.toSet()) extraNotesToBroadcast.add(it.first)
extraNotesToBroadcast.add(it.second)
} }
} }
} }
accountViewModel?.account?.signAndSendPrivatelyOrBroadcast( tryAndWait { continuation ->
template, accountViewModel?.account?.signAndComputeBroadcast(template, extraNotesToBroadcast) {
relayList = { relayList }, continuation.resume(it)
) }
}
accountViewModel?.deleteDraft(draftTag.current) accountViewModel?.deleteDraft(draftTag.current)
@@ -526,7 +524,7 @@ open class ShortNotePostViewModel :
val zapReceiver = if (wantsForwardZapTo) forwardZapTo.value.toZapSplitSetup() else null val zapReceiver = if (wantsForwardZapTo) forwardZapTo.value.toZapSplitSetup() else null
val geoHash = (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() val geoHash = (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString()
val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount.value else null val localZapRaiserAmount = if (wantsZapRaiser) zapRaiserAmount.value else null
val emojis = findEmoji(tagger.message, account?.emoji?.myEmojis?.value) val emojis = findEmoji(tagger.message, account?.emoji?.myEmojis?.value)
val urls = findURLs(tagger.message) val urls = findURLs(tagger.message)
@@ -565,7 +563,7 @@ open class ShortNotePostViewModel :
replyingTo = originalNote?.toEventHint<TextNoteEvent>(), replyingTo = originalNote?.toEventHint<TextNoteEvent>(),
forkingFrom = forkedFromNote?.toEventHint<TextNoteEvent>(), forkingFrom = forkedFromNote?.toEventHint<TextNoteEvent>(),
) { ) {
tagger.pTags?.let { notify(it.map { it.toPTag() }) } tagger.pTags?.let { pTagList -> notify(pTagList.map { it.toPTag() }) }
hashtags(findHashtags(tagger.message)) hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message)) references(findURLs(tagger.message))
@@ -619,9 +617,9 @@ open class ShortNotePostViewModel :
) )
if (results.allGood) { if (results.allGood) {
results.successful.forEach { results.successful.forEach { state ->
if (it.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) {
account?.createNip95(it.result.bytes, headerInfo = it.result.fileHeader, alt, contentWarningReason) { nip95 -> account?.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason) { nip95 ->
nip95attachments = nip95attachments + nip95 nip95attachments = nip95attachments + nip95
val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) } val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) }
@@ -630,20 +628,20 @@ open class ShortNotePostViewModel :
urlPreviews.update(message) urlPreviews.update(message)
} }
} }
} else if (it.result is UploadOrchestrator.OrchestratorResult.ServerResult) { } else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
val iMeta = val iMeta =
IMetaTagBuilder(it.result.url) IMetaTagBuilder(state.result.url)
.apply { .apply {
hash(it.result.fileHeader.hash) hash(state.result.fileHeader.hash)
size(it.result.fileHeader.size) size(state.result.fileHeader.size)
it.result.fileHeader.mimeType state.result.fileHeader.mimeType
?.let { mimeType(it) } ?.let { mimeType(it) }
it.result.fileHeader.dim state.result.fileHeader.dim
?.let { dims(it) } ?.let { dims(it) }
it.result.fileHeader.blurHash state.result.fileHeader.blurHash
?.let { blurhash(it.blurhash) } ?.let { blurhash(it.blurhash) }
it.result.magnet?.let { magnet(it) } state.result.magnet?.let { magnet(it) }
it.result.uploadedHash?.let { originalHash(it) } state.result.uploadedHash?.let { originalHash(it) }
alt?.let { alt(it) } alt?.let { alt(it) }
contentWarningReason?.let { sensitiveContent(contentWarningReason) } contentWarningReason?.let { sensitiveContent(contentWarningReason) }
@@ -651,7 +649,7 @@ open class ShortNotePostViewModel :
iMetaAttachments.replace(iMeta.url, iMeta) iMetaAttachments.replace(iMeta.url, iMeta)
message = message.insertUrlAtCursor(it.result.url) message = message.insertUrlAtCursor(state.result.url)
urlPreviews.update(message) urlPreviews.update(message)
} }
} }
@@ -677,7 +675,7 @@ open class ShortNotePostViewModel :
pTags = null pTags = null
wantsPoll = false wantsPoll = false
zapRecipients = mutableStateListOf<HexKey>() zapRecipients = mutableStateListOf()
pollOptions = newStateMapPollOptions() pollOptions = newStateMapPollOptions()
valueMaximum = null valueMaximum = null
valueMinimum = null valueMinimum = null
@@ -685,7 +683,7 @@ open class ShortNotePostViewModel :
closedAt = null closedAt = null
wantsInvoice = false wantsInvoice = false
wantsZapraiser = false wantsZapRaiser = false
zapRaiserAmount.value = null zapRaiserAmount.value = null
wantsForwardZapTo = false wantsForwardZapTo = false
@@ -706,21 +704,9 @@ open class ShortNotePostViewModel :
emojiSuggestions?.reset() emojiSuggestions?.reset()
showRelaysDialog = false
reloadRelaySet()
draftTag.rotate() draftTag.rotate()
} }
fun reloadRelaySet() {
val account = accountViewModel?.account ?: return
relayList =
account.outboxRelays.flow.value
.toImmutableList()
}
fun deleteMediaToUpload(selected: SelectedMediaProcessing) { fun deleteMediaToUpload(selected: SelectedMediaProcessing) {
this.multiOrchestrator?.remove(selected) this.multiOrchestrator?.remove(selected)
} }
@@ -733,8 +719,8 @@ open class ShortNotePostViewModel :
updateMessage(TextFieldValue(message.text + " " + it)) updateMessage(TextFieldValue(message.text + " " + it))
} }
override fun updateMessage(it: TextFieldValue) { override fun updateMessage(newMessage: TextFieldValue) {
message = it message = newMessage
urlPreviews.update(message) urlPreviews.update(message)
if (message.selection.collapsed) { if (message.selection.collapsed) {
@@ -793,8 +779,12 @@ open class ShortNotePostViewModel :
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
iMetaAttachments.downloadAndPrepare( iMetaAttachments.downloadAndPrepare(
item.link.url, item.link.url,
{ Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) }, ) {
) Amethyst.instance.okHttpClients.getHttpClient(
accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url)
?: false,
)
}
} }
message = message.replaceCurrentWord(wordToInsert) message = message.replaceCurrentWord(wordToInsert)
@@ -811,13 +801,13 @@ open class ShortNotePostViewModel :
message.text.isNotBlank() && message.text.isNotBlank() &&
!isUploadingImage && !isUploadingImage &&
!wantsInvoice && !wantsInvoice &&
(!wantsZapraiser || zapRaiserAmount.value != null) && (!wantsZapRaiser || zapRaiserAmount.value != null) &&
( (
!wantsPoll || !wantsPoll ||
( (
pollOptions.values.all { it.isNotEmpty() } && pollOptions.values.all { it.isNotEmpty() } &&
isValidvalueMinimum.value && isValidValueMinimum.value &&
isValidvalueMaximum.value isValidValueMaximum.value
) )
) && ) &&
multiOrchestrator == null multiOrchestrator == null
@@ -857,11 +847,11 @@ open class ShortNotePostViewModel :
fun checkMinMax() { fun checkMinMax() {
if ((valueMinimum ?: 0) > (valueMaximum ?: Long.MAX_VALUE)) { if ((valueMinimum ?: 0) > (valueMaximum ?: Long.MAX_VALUE)) {
isValidvalueMinimum.value = false isValidValueMinimum.value = false
isValidvalueMaximum.value = false isValidValueMaximum.value = false
} else { } else {
isValidvalueMinimum.value = true isValidValueMinimum.value = true
isValidvalueMaximum.value = true isValidValueMaximum.value = true
} }
} }
@@ -237,18 +237,18 @@ class NostrClient(
} }
fun sendIfExists( fun sendIfExists(
signedEvent: Event, event: Event,
connectedRelay: NormalizedRelayUrl, connectedRelay: NormalizedRelayUrl,
) { ) {
relayPool.getRelay(connectedRelay)?.send(signedEvent) relayPool.getRelay(connectedRelay)?.send(event)
} }
fun send( fun send(
signedEvent: Event, event: Event,
relayList: Set<NormalizedRelayUrl>, relayList: Set<NormalizedRelayUrl>,
) { ) {
eventOutbox.markAsSending(signedEvent, relayList) eventOutbox.markAsSending(event, relayList)
relayPool.send(signedEvent, relayList) relayPool.send(event, relayList)
} }
fun close(subscriptionId: String) { fun close(subscriptionId: String) {