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.hash
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.HexKey
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
@@ -1085,14 +1086,15 @@ class Account(
fun consumeAndSendNip95(
data: FileStorageEvent,
signedEvent: FileStorageHeaderEvent,
relayList: List<NormalizedRelayUrl>,
): Note? {
if (!isWriteable()) return null
client.send(data, relayList = relayList.toSet())
val relayList = computeRelayListToBroadcast(signedEvent)
client.send(data, relayList = relayList)
cache.justConsumeMyOwnEvent(data)
client.send(signedEvent, relayList = relayList.toSet())
client.send(signedEvent, relayList = relayList)
cache.justConsumeMyOwnEvent(signedEvent)
return cache.getNoteIfExists(signedEvent.id)
@@ -1161,7 +1163,6 @@ class Account(
urlHeaderInfo: Map<String, FileHeader>,
caption: String?,
contentWarningReason: String?,
relayList: Set<NormalizedRelayUrl>,
onReady: (Note) -> Unit,
) {
val iMetas =
@@ -1180,7 +1181,7 @@ class Account(
)
}
signer.sign(
val template =
PictureEvent.build(iMetas, caption ?: "") {
caption?.let {
hashtags(findHashtags(it))
@@ -1192,9 +1193,10 @@ class Account(
// add geohashes
// add title
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?,
contentWarningReason: String?,
originalHash: String? = null,
relayList: Set<NormalizedRelayUrl>,
onReady: (Note) -> Unit,
) {
if (!isWriteable()) return
@@ -1273,8 +1274,8 @@ class Account(
}
}
signer.sign(template) {
sendHeader(it, relayList = relayList, onReady)
signAndComputeBroadcast(template) { event ->
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(
template: EventTemplate<T>,
relayList: Set<NormalizedRelayUrl>,
@@ -1581,7 +1606,7 @@ class Account(
originalNote: Note,
notify: HexKey?,
summary: String? = null,
relayList: List<NormalizedRelayUrl>,
broadcast: List<Event>,
) {
if (!isWriteable()) return
@@ -1593,9 +1618,14 @@ class Account(
notify = notify,
summary = summary,
signer = signer,
) {
cache.justConsumeMyOwnEvent(it)
client.send(it, relayList = relayList.toSet())
) { event ->
cache.justConsumeMyOwnEvent(event)
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()
if (relayList != null) {
client.send(signedEvent = wrap, relayList = relayList)
client.send(event = wrap, relayList = relayList)
} else {
val taggedUserInboxRelays =
wrap.taggedUserIds().flatMapTo(mutableSetOf()) { pubkey ->
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.actions
import androidx.compose.foundation.border
import androidx.compose.foundation.horizontalScroll
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
@@ -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.uploads.ImageVideoDescription
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.stringRes
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.replyModifier
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -128,12 +125,6 @@ fun EditPostView(
val scrollState = rememberScrollState()
val scope = rememberCoroutineScope()
var showRelaysDialog by remember { mutableStateOf(false) }
var relayList =
remember {
accountViewModel.account.outboxRelays.flow.value
.toImmutableList()
}
LaunchedEffect(Unit) {
postViewModel.load(edit, versionLookingAt, accountViewModel)
@@ -148,16 +139,6 @@ fun EditPostView(
decorFitsSystemWindows = false,
),
) {
if (showRelaysDialog) {
RelaySelectionDialog(
preSelectedList = relayList,
onClose = { showRelaysDialog = false },
onPost = { relayList = it },
accountViewModel = accountViewModel,
nav = nav,
)
}
Scaffold(
topBar = {
TopAppBar(
@@ -169,22 +150,9 @@ fun EditPostView(
) {
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(
onPost = {
postViewModel.sendPost(relayList = relayList)
postViewModel.sendPost()
scope.launch {
delay(100)
onClose()
@@ -47,7 +47,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent
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.IMetaTagBuilder
import com.vitorpamplona.quartz.nip94FileMetadata.alt
@@ -120,18 +120,21 @@ open class EditPostViewModel : ViewModel() {
editedFromNote = edit
}
fun sendPost(relayList: List<NormalizedRelayUrl>) {
viewModelScope.launch(Dispatchers.IO) { innerSendPost(relayList) }
fun sendPost() {
viewModelScope.launch(Dispatchers.IO) { innerSendPost() }
}
suspend fun innerSendPost(relayList: List<NormalizedRelayUrl>) {
suspend fun innerSendPost() {
if (accountViewModel == null) {
cancel()
return
}
val extraNotesToBroadcast = mutableListOf<Event>()
nip95attachments.forEach {
account?.sendNip95(it.first, it.second, relayList.toSet())
extraNotesToBroadcast.add(it.first)
extraNotesToBroadcast.add(it.second)
}
val notify =
@@ -147,7 +150,7 @@ open class EditPostViewModel : ViewModel() {
originalNote = editedFromNote!!,
notify = notify,
summary = subject.text.ifBlank { null },
relayList = relayList,
extraNotesToBroadcast,
)
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.SelectedMediaProcessing
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.joinAll
@@ -82,13 +81,11 @@ open class NewMediaModel : ViewModel() {
fun upload(
context: Context,
relayList: List<NormalizedRelayUrl>,
onSucess: () -> Unit,
onError: (String, String) -> Unit,
) {
viewModelScope.launch {
val myAccount = account ?: return@launch
if (relayList.isEmpty()) return@launch
val serverToUse = selectedServer ?: return@launch
val myMultiOrchestrator = multiOrchestrator ?: return@launch
@@ -141,7 +138,7 @@ open class NewMediaModel : ViewModel() {
withTimeoutOrNull(30000) {
suspendCancellableCoroutine { continuation ->
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)
}
}
@@ -156,13 +153,12 @@ open class NewMediaModel : ViewModel() {
withTimeoutOrNull(30000) {
suspendCancellableCoroutine { continuation ->
account?.sendHeader(
it.url,
it.magnet,
it.fileHeader,
caption,
if (sensitiveContent) "" else null,
it.uploadedHash,
relayList.toSet(),
url = it.url,
magnetUri = it.magnet,
headerInfo = it.fileHeader,
alt = caption,
contentWarningReason = if (sensitiveContent) "" else null,
originalHash = it.uploadedHash,
) {
continuation.resume(true)
}
@@ -178,10 +174,9 @@ open class NewMediaModel : ViewModel() {
withTimeoutOrNull(30000) {
suspendCancellableCoroutine { continuation ->
account?.sendAllAsOnePictureEvent(
imageUrls,
caption,
if (sensitiveContent) "" else null,
relayList.toSet(),
urlHeaderInfo = imageUrls,
caption = caption,
contentWarningReason = if (sensitiveContent) "" else null,
) {
continuation.resume(true)
}
@@ -35,8 +35,6 @@ 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
@@ -49,7 +47,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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
@@ -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.PostButton
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.TextSpinner
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
@@ -100,13 +96,6 @@ fun NewMediaView(
postViewModel.load(account, uris)
}
var showRelaysDialog by remember { mutableStateOf(false) }
var relayList =
remember {
accountViewModel.account.outboxRelays.flow.value
.toImmutableList()
}
Dialog(
onDismissRequest = { onClose() },
properties =
@@ -128,23 +117,9 @@ fun NewMediaView(
) {
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(
onPost = {
postViewModel.upload(context, relayList, onClose, accountViewModel.toastManager::toast)
postViewModel.upload(context, onClose, accountViewModel.toastManager::toast)
postViewModel.selectedServer?.let {
if (it.type != ServerType.NIP95) {
account.settings.changeDefaultFileServer(it)
@@ -173,16 +148,6 @@ fun NewMediaView(
)
},
) { pad ->
if (showRelaysDialog) {
RelaySelectionDialog(
preSelectedList = relayList,
onClose = { showRelaysDialog = false },
onPost = { relayList = it },
accountViewModel = accountViewModel,
nav = nav,
)
}
Surface(
modifier =
Modifier
@@ -66,7 +66,7 @@ fun NewPollVoteValueRange(pollViewModel: ShortNotePostViewModel) {
onValueChange = { pollViewModel.updateMinZapAmountForPoll(it) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.weight(1f),
colors = if (pollViewModel.isValidvalueMinimum.value) colorValid else colorInValid,
colors = if (pollViewModel.isValidValueMinimum.value) colorValid else colorInValid,
label = {
Text(
text = stringRes(R.string.poll_zap_value_min),
@@ -88,7 +88,7 @@ fun NewPollVoteValueRange(pollViewModel: ShortNotePostViewModel) {
onValueChange = { pollViewModel.updateMaxZapAmountForPoll(it) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.weight(1f),
colors = if (pollViewModel.isValidvalueMaximum.value) colorValid else colorInValid,
colors = if (pollViewModel.isValidValueMaximum.value) colorValid else colorInValid,
label = {
Text(
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.nip01Core.core.AddressableEvent
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.tags.geohash.geohash
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.sensitiveContent
import com.vitorpamplona.quartz.nip94FileMetadata.size
import com.vitorpamplona.quartz.utils.tryAndWait
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlin.collections.plus
import kotlin.coroutines.resume
@Stable
open class CommentPostViewModel :
@@ -131,7 +131,7 @@ open class CommentPostViewModel :
var account: Account? = null
var externalIdentity by mutableStateOf<ExternalId?>(null)
var replyingTo: Note? by mutableStateOf<Note?>(null)
var replyingTo: Note? by mutableStateOf(null)
val iMetaAttachments = IMetaAttachments()
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(
@@ -177,9 +177,6 @@ open class CommentPostViewModel :
var wantsZapraiser by mutableStateOf(false)
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 hasLnAddress(): Boolean = account?.userProfile()?.info?.lnAddress() != null
@@ -270,15 +267,15 @@ open class CommentPostViewModel :
canAddZapRaiser = accountViewModel?.userProfile()?.info?.lnAddress() != 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()
localfowardZapTo.forEach {
localForwardZapTo.forEach {
val user = LocalCache.getOrCreateUser(it[1])
val value = it.last().toFloatOrNull() ?: 0f
forwardZapTo.value.addItem(user, value)
}
forwardZapToEditting.value = TextFieldValue("")
wantsForwardZapTo = localfowardZapTo.isNotEmpty()
wantsForwardZapTo = localForwardZapTo.isNotEmpty()
wantsToMarkAsSensitive = draftEvent.isSensitive()
@@ -311,21 +308,23 @@ open class CommentPostViewModel :
suspend fun sendPostSync() {
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()
nip95attachments.forEach {
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(
template,
relayList = { relayList },
)
tryAndWait { continuation ->
accountViewModel?.account?.signAndComputeBroadcast(template, extraNotesToBroadcast) {
continuation.resume(it)
}
}
accountViewModel?.deleteDraft(draftTag.current)
@@ -378,7 +377,7 @@ open class CommentPostViewModel :
msg = tagger.message,
replyingTo = eventHint,
) {
tagger.pTags?.let { notify(it.map { it.toPTag() }) }
tagger.pTags?.let { pTagList -> notify(pTagList.map { it.toPTag() }) }
hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message))
@@ -398,7 +397,7 @@ open class CommentPostViewModel :
msg = tagger.message,
extId = externalIdentity,
) {
tagger.pTags?.let { notify(it.map { it.toPTag() }) }
tagger.pTags?.let { pTagList -> notify(pTagList.map { it.toPTag() }) }
hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message))
@@ -458,9 +457,9 @@ open class CommentPostViewModel :
)
if (results.allGood) {
results.successful.forEach {
if (it.result is UploadOrchestrator.OrchestratorResult.NIP95Result) {
account?.createNip95(it.result.bytes, headerInfo = it.result.fileHeader, alt, contentWarningReason) { nip95 ->
results.successful.forEach { state ->
if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) {
account?.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason) { nip95 ->
nip95attachments = nip95attachments + nip95
val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) }
@@ -469,20 +468,20 @@ open class CommentPostViewModel :
urlPreviews.update(message)
}
}
} else if (it.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
} else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
val iMeta =
IMetaTagBuilder(it.result.url)
IMetaTagBuilder(state.result.url)
.apply {
hash(it.result.fileHeader.hash)
size(it.result.fileHeader.size)
it.result.fileHeader.mimeType
hash(state.result.fileHeader.hash)
size(state.result.fileHeader.size)
state.result.fileHeader.mimeType
?.let { mimeType(it) }
it.result.fileHeader.dim
state.result.fileHeader.dim
?.let { dims(it) }
it.result.fileHeader.blurHash
state.result.fileHeader.blurHash
?.let { blurhash(it.blurhash) }
it.result.magnet?.let { magnet(it) }
it.result.uploadedHash?.let { originalHash(it) }
state.result.magnet?.let { magnet(it) }
state.result.uploadedHash?.let { originalHash(it) }
alt?.let { alt(it) }
contentWarningReason?.let { sensitiveContent(contentWarningReason) }
@@ -490,7 +489,7 @@ open class CommentPostViewModel :
iMetaAttachments.replace(iMeta.url, iMeta)
message = message.insertUrlAtCursor(it.result.url)
message = message.insertUrlAtCursor(state.result.url)
urlPreviews.update(message)
}
}
@@ -546,21 +545,9 @@ open class CommentPostViewModel :
emojiSuggestions?.reset()
showRelaysDialog = false
reloadRelaySet()
draftTag.rotate()
}
fun reloadRelaySet() {
val account = accountViewModel?.account ?: return
relayList =
account.outboxRelays.flow.value
.toImmutableList()
}
fun deleteMediaToUpload(selected: SelectedMediaProcessing) {
this.multiOrchestrator?.remove(selected)
}
@@ -569,8 +556,8 @@ open class CommentPostViewModel :
notifying = notifying?.filter { it != userToRemove }
}
override fun updateMessage(it: TextFieldValue) {
message = it
override fun updateMessage(newMessage: TextFieldValue) {
message = newMessage
urlPreviews.update(message)
if (message.selection.collapsed) {
@@ -627,13 +614,13 @@ open class CommentPostViewModel :
val wordToInsert = item.link.url + " "
viewModelScope.launch(Dispatchers.IO) {
iMetaAttachments.downloadAndPrepare(
item.link.url,
{
Amethyst.Companion.instance.okHttpClients
.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false)
},
)
iMetaAttachments.downloadAndPrepare(item.link.url) {
Amethyst.Companion.instance.okHttpClients
.getHttpClient(
accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url)
?: false,
)
}
}
message = message.replaceCurrentWord(wordToInsert)
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.note.nip22Comments
import android.net.Uri
import androidx.compose.foundation.horizontalScroll
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
@@ -37,8 +36,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
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.Scaffold
import androidx.compose.material3.Surface
@@ -46,7 +43,6 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -56,7 +52,6 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
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.uploads.SelectFromGallery
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.zapsplits.ForwardZapTo
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.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size10dp
@@ -116,7 +110,6 @@ fun ReplyCommentPostScreen(
val context = LocalContext.current
LaunchedEffect(Unit) {
postViewModel.reloadRelaySet()
reply?.let {
postViewModel.reply(it)
}
@@ -159,20 +152,6 @@ fun GenericCommentPostScreen(
verticalAlignment = CenterVertically,
) {
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(
onPost = {
// uses the accountViewModel scope to avoid cancelling this
@@ -209,15 +188,6 @@ fun GenericCommentPostScreen(
)
},
) { pad ->
if (postViewModel.showRelaysDialog) {
RelaySelectionDialog(
preSelectedList = postViewModel.relayList ?: persistentListOf(),
onClose = { postViewModel.showRelaysDialog = false },
onPost = { postViewModel.relayList = it },
accountViewModel = accountViewModel,
nav = nav,
)
}
Surface(
modifier =
Modifier
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds
import android.net.Uri
import androidx.compose.foundation.horizontalScroll
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
@@ -37,8 +36,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
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.Scaffold
import androidx.compose.material3.Surface
@@ -47,10 +44,7 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -60,7 +54,6 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
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.uploads.SelectFromGallery
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.zapsplits.ForwardZapTo
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.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size10dp
@@ -151,8 +143,6 @@ fun NewProductScreen(
) {
WatchAndLoadMyEmojiList(accountViewModel)
var showRelaysDialog by remember { mutableStateOf(false) }
Scaffold(
topBar = {
TopAppBar(
@@ -164,19 +154,6 @@ fun NewProductScreen(
) {
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(
onPost = {
accountViewModel.viewModelScope.launch(Dispatchers.IO) {
@@ -210,15 +187,6 @@ fun NewProductScreen(
)
},
) { pad ->
if (showRelaysDialog) {
RelaySelectionDialog(
preSelectedList = postViewModel.relayList ?: persistentListOf(),
onClose = { showRelaysDialog = false },
onPost = { postViewModel.relayList = it },
accountViewModel = accountViewModel,
nav = nav,
)
}
Surface(
modifier =
Modifier
@@ -54,7 +54,6 @@ fun GeoHashPostScreen(
val context = LocalContext.current
LaunchedEffect(Unit) {
postViewModel.reloadRelaySet()
geohash?.let {
postViewModel.newPostFor(GeohashId(it))
}
@@ -54,7 +54,6 @@ fun HashtagPostScreen(
val context = LocalContext.current
LaunchedEffect(Unit) {
postViewModel.reloadRelaySet()
hashtag?.let {
postViewModel.newPostFor(HashtagId(it))
}
@@ -25,7 +25,6 @@ import android.net.Uri
import android.os.Parcelable
import androidx.compose.foundation.horizontalScroll
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
@@ -50,8 +49,6 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
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.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -62,7 +59,6 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
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.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
@@ -173,8 +169,6 @@ private fun NewPostScreenInner(
accountViewModel: AccountViewModel,
nav: Nav,
) {
val scope = rememberCoroutineScope()
WatchAndLoadMyEmojiList(accountViewModel)
Scaffold(
@@ -188,19 +182,6 @@ private fun NewPostScreenInner(
) {
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(
onPost = {
// uses the accountViewModel scope to avoid cancelling this
@@ -238,15 +219,6 @@ private fun NewPostScreenInner(
)
},
) { pad ->
if (postViewModel.showRelaysDialog) {
RelaySelectionDialog(
preSelectedList = postViewModel.relayList ?: persistentListOf(),
onClose = { postViewModel.showRelaysDialog = false },
onPost = { postViewModel.relayList = it },
accountViewModel = accountViewModel,
nav = nav,
)
}
Surface(
modifier =
Modifier
@@ -419,7 +391,7 @@ private fun NewPostScreenBody(
}
}
if (postViewModel.wantsZapraiser && postViewModel.hasLnAddress()) {
if (postViewModel.wantsZapRaiser && postViewModel.hasLnAddress()) {
Row(
verticalAlignment = CenterVertically,
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
@@ -495,8 +467,8 @@ private fun BottomRowActions(postViewModel: ShortNotePostViewModel) {
}
if (postViewModel.canAddZapRaiser) {
AddZapraiserButton(postViewModel.wantsZapraiser) {
postViewModel.wantsZapraiser = !postViewModel.wantsZapraiser
AddZapraiserButton(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.Event
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.tags.geohash.geohash
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.sensitiveContent
import com.vitorpamplona.quartz.nip94FileMetadata.size
import com.vitorpamplona.quartz.utils.tryAndWait
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlin.coroutines.resume
enum class UserSuggestionAnchor {
MAIN_MESSAGE,
@@ -151,8 +151,8 @@ open class ShortNotePostViewModel :
var accountViewModel: AccountViewModel? = null
var account: Account? = null
var originalNote: Note? by mutableStateOf<Note?>(null)
var forkedFromNote: Note? by mutableStateOf<Note?>(null)
var originalNote: Note? by mutableStateOf(null)
var forkedFromNote: Note? by mutableStateOf(null)
var pTags by mutableStateOf<List<User>?>(null)
var eTags by mutableStateOf<List<Note>?>(null)
@@ -185,9 +185,8 @@ open class ShortNotePostViewModel :
var consensusThreshold: Int? = null
var closedAt: Long? = null
var isValidRecipients = mutableStateOf(true)
var isValidvalueMaximum = mutableStateOf(true)
var isValidvalueMinimum = mutableStateOf(true)
var isValidValueMaximum = mutableStateOf(true)
var isValidValueMinimum = mutableStateOf(true)
var isValidConsensusThreshold = mutableStateOf(true)
var isValidClosedAt = mutableStateOf(true)
@@ -212,12 +211,9 @@ open class ShortNotePostViewModel :
// ZapRaiser
var canAddZapRaiser by mutableStateOf(false)
var wantsZapraiser by mutableStateOf(false)
var wantsZapRaiser by mutableStateOf(false)
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 hasLnAddress(): Boolean = account?.userProfile()?.info?.lnAddress() != null
@@ -295,10 +291,10 @@ open class ShortNotePostViewModel :
canUsePoll = originalNote == null
multiOrchestrator = null
quote?.let {
message = TextFieldValue(message.text + "\nnostr:${it.toNEvent()}")
quote?.let { quotedNote ->
message = TextFieldValue(message.text + "\nnostr:${quotedNote.toNEvent()}")
it.author?.let { quotedUser ->
quotedNote.author?.let { quotedUser ->
if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
if (forwardZapTo.value.items.none { it.key.pubkeyHex == quotedUser.pubkeyHex }) {
forwardZapTo.value.addItem(quotedUser)
@@ -313,21 +309,21 @@ open class ShortNotePostViewModel :
}
}
fork?.let {
message = TextFieldValue(version?.event?.content ?: it.event?.content ?: "")
fork?.let { forkedNoted ->
message = TextFieldValue(version?.event?.content ?: forkedNoted.event?.content ?: "")
it.event?.isSensitiveOrNSFW()?.let {
forkedNoted.event?.isSensitiveOrNSFW()?.let {
if (it) wantsToMarkAsSensitive = true
}
it.event?.zapraiserAmount()?.let {
forkedNoted.event?.zapraiserAmount()?.let {
zapRaiserAmount.value = it
}
it.event?.zapSplitSetup()?.let {
val totalWeight = it.sumOf { if (it is ZapSplitSetupLnAddress) 0.0 else it.weight }
forkedNoted.event?.zapSplitSetup()?.let { setup ->
val totalWeight = setup.sumOf { if (it is ZapSplitSetupLnAddress) 0.0 else it.weight }
it.forEach {
setup.forEach {
if (it is ZapSplitSetup) {
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.
if (forwardZapTo.value.items.isEmpty()) {
it.author?.let { forkedAuthor ->
forkedNoted.author?.let { forkedAuthor ->
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 == 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) {
this.pTags = listOf(it)
} else if (this.pTags?.contains(it) != true) {
@@ -355,7 +351,7 @@ open class ShortNotePostViewModel :
}
}
forkedFromNote = it
forkedFromNote = forkedNoted
} ?: run {
forkedFromNote = null
}
@@ -380,15 +376,15 @@ open class ShortNotePostViewModel :
canAddZapRaiser = accountViewModel?.userProfile()?.info?.lnAddress() != 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()
localfowardZapTo.forEach {
localForwardZapTo.forEach {
val user = LocalCache.getOrCreateUser(it[1])
val value = it.last().toFloatOrNull() ?: 0f
forwardZapTo.value.addItem(user, value)
}
forwardZapToEditting.value = TextFieldValue("")
wantsForwardZapTo = localfowardZapTo.isNotEmpty()
wantsForwardZapTo = localForwardZapTo.isNotEmpty()
wantsToMarkAsSensitive = draftEvent.isSensitive()
@@ -398,11 +394,11 @@ open class ShortNotePostViewModel :
wantsExclusiveGeoPost = draftEvent.kind == CommentEvent.KIND
}
val zapraiser = draftEvent.zapraiserAmount()
wantsZapraiser = zapraiser != null
val zapRaiser = draftEvent.zapraiserAmount()
wantsZapRaiser = zapRaiser != null
zapRaiserAmount.value = null
if (zapraiser != null) {
zapRaiserAmount.value = zapraiser
if (zapRaiser != null) {
zapRaiserAmount.value = zapRaiser
}
eTags =
@@ -416,7 +412,7 @@ open class ShortNotePostViewModel :
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])
forkedFromNote = note
}
@@ -470,21 +466,23 @@ open class ShortNotePostViewModel :
suspend fun sendPostSync() {
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()
nip95attachments.forEach {
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(
template,
relayList = { relayList },
)
tryAndWait { continuation ->
accountViewModel?.account?.signAndComputeBroadcast(template, extraNotesToBroadcast) {
continuation.resume(it)
}
}
accountViewModel?.deleteDraft(draftTag.current)
@@ -526,7 +524,7 @@ open class ShortNotePostViewModel :
val zapReceiver = if (wantsForwardZapTo) forwardZapTo.value.toZapSplitSetup() else null
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 urls = findURLs(tagger.message)
@@ -565,7 +563,7 @@ open class ShortNotePostViewModel :
replyingTo = originalNote?.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))
references(findURLs(tagger.message))
@@ -619,9 +617,9 @@ open class ShortNotePostViewModel :
)
if (results.allGood) {
results.successful.forEach {
if (it.result is UploadOrchestrator.OrchestratorResult.NIP95Result) {
account?.createNip95(it.result.bytes, headerInfo = it.result.fileHeader, alt, contentWarningReason) { nip95 ->
results.successful.forEach { state ->
if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) {
account?.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason) { nip95 ->
nip95attachments = nip95attachments + nip95
val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) }
@@ -630,20 +628,20 @@ open class ShortNotePostViewModel :
urlPreviews.update(message)
}
}
} else if (it.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
} else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
val iMeta =
IMetaTagBuilder(it.result.url)
IMetaTagBuilder(state.result.url)
.apply {
hash(it.result.fileHeader.hash)
size(it.result.fileHeader.size)
it.result.fileHeader.mimeType
hash(state.result.fileHeader.hash)
size(state.result.fileHeader.size)
state.result.fileHeader.mimeType
?.let { mimeType(it) }
it.result.fileHeader.dim
state.result.fileHeader.dim
?.let { dims(it) }
it.result.fileHeader.blurHash
state.result.fileHeader.blurHash
?.let { blurhash(it.blurhash) }
it.result.magnet?.let { magnet(it) }
it.result.uploadedHash?.let { originalHash(it) }
state.result.magnet?.let { magnet(it) }
state.result.uploadedHash?.let { originalHash(it) }
alt?.let { alt(it) }
contentWarningReason?.let { sensitiveContent(contentWarningReason) }
@@ -651,7 +649,7 @@ open class ShortNotePostViewModel :
iMetaAttachments.replace(iMeta.url, iMeta)
message = message.insertUrlAtCursor(it.result.url)
message = message.insertUrlAtCursor(state.result.url)
urlPreviews.update(message)
}
}
@@ -677,7 +675,7 @@ open class ShortNotePostViewModel :
pTags = null
wantsPoll = false
zapRecipients = mutableStateListOf<HexKey>()
zapRecipients = mutableStateListOf()
pollOptions = newStateMapPollOptions()
valueMaximum = null
valueMinimum = null
@@ -685,7 +683,7 @@ open class ShortNotePostViewModel :
closedAt = null
wantsInvoice = false
wantsZapraiser = false
wantsZapRaiser = false
zapRaiserAmount.value = null
wantsForwardZapTo = false
@@ -706,21 +704,9 @@ open class ShortNotePostViewModel :
emojiSuggestions?.reset()
showRelaysDialog = false
reloadRelaySet()
draftTag.rotate()
}
fun reloadRelaySet() {
val account = accountViewModel?.account ?: return
relayList =
account.outboxRelays.flow.value
.toImmutableList()
}
fun deleteMediaToUpload(selected: SelectedMediaProcessing) {
this.multiOrchestrator?.remove(selected)
}
@@ -733,8 +719,8 @@ open class ShortNotePostViewModel :
updateMessage(TextFieldValue(message.text + " " + it))
}
override fun updateMessage(it: TextFieldValue) {
message = it
override fun updateMessage(newMessage: TextFieldValue) {
message = newMessage
urlPreviews.update(message)
if (message.selection.collapsed) {
@@ -793,8 +779,12 @@ open class ShortNotePostViewModel :
viewModelScope.launch(Dispatchers.IO) {
iMetaAttachments.downloadAndPrepare(
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)
@@ -811,13 +801,13 @@ open class ShortNotePostViewModel :
message.text.isNotBlank() &&
!isUploadingImage &&
!wantsInvoice &&
(!wantsZapraiser || zapRaiserAmount.value != null) &&
(!wantsZapRaiser || zapRaiserAmount.value != null) &&
(
!wantsPoll ||
(
pollOptions.values.all { it.isNotEmpty() } &&
isValidvalueMinimum.value &&
isValidvalueMaximum.value
isValidValueMinimum.value &&
isValidValueMaximum.value
)
) &&
multiOrchestrator == null
@@ -857,11 +847,11 @@ open class ShortNotePostViewModel :
fun checkMinMax() {
if ((valueMinimum ?: 0) > (valueMaximum ?: Long.MAX_VALUE)) {
isValidvalueMinimum.value = false
isValidvalueMaximum.value = false
isValidValueMinimum.value = false
isValidValueMaximum.value = false
} else {
isValidvalueMinimum.value = true
isValidvalueMaximum.value = true
isValidValueMinimum.value = true
isValidValueMaximum.value = true
}
}
@@ -237,18 +237,18 @@ class NostrClient(
}
fun sendIfExists(
signedEvent: Event,
event: Event,
connectedRelay: NormalizedRelayUrl,
) {
relayPool.getRelay(connectedRelay)?.send(signedEvent)
relayPool.getRelay(connectedRelay)?.send(event)
}
fun send(
signedEvent: Event,
event: Event,
relayList: Set<NormalizedRelayUrl>,
) {
eventOutbox.markAsSending(signedEvent, relayList)
relayPool.send(signedEvent, relayList)
eventOutbox.markAsSending(event, relayList)
relayPool.send(event, relayList)
}
fun close(subscriptionId: String) {