Adds a card to setup the DM relay list if the user doesn't have one.

This commit is contained in:
Vitor Pamplona
2024-05-22 17:46:39 -04:00
parent 76103ac057
commit 599fddb369
11 changed files with 900 additions and 82 deletions
@@ -52,6 +52,7 @@ import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.ChatMessageEvent
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.Contact
import com.vitorpamplona.quartz.events.ContactListEvent
@@ -2545,6 +2546,40 @@ class Account(
}
}
fun getDMRelayList(): ChatMessageRelayListEvent? {
return LocalCache.getOrCreateAddressableNote(
ChatMessageRelayListEvent.createAddressATag(signer.pubKey),
).event as? ChatMessageRelayListEvent
}
fun saveDMRelayList(dmRelays: List<String>) {
if (!isWriteable()) return
val relayListForDMs =
LocalCache.getOrCreateAddressableNote(
ChatMessageRelayListEvent.createAddressATag(signer.pubKey),
).event as? ChatMessageRelayListEvent
if (relayListForDMs != null && relayListForDMs.tags.isNotEmpty()) {
ChatMessageRelayListEvent.updateRelayList(
earlierVersion = relayListForDMs,
relays = dmRelays,
signer = signer,
) {
Client.send(it)
LocalCache.justConsume(it, null)
}
} else {
ChatMessageRelayListEvent.createFromScratch(
relays = dmRelays,
signer = signer,
) {
Client.send(it)
LocalCache.justConsume(it, null)
}
}
}
fun setHideDeleteRequestDialog() {
hideDeleteRequestDialog = true
saveable.invalidateData()
@@ -0,0 +1,559 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.DeleteSweep
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Paid
import androidx.compose.material.icons.filled.SyncProblem
import androidx.compose.material.icons.filled.Upload
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChat
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.WarningColor
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.amethyst.ui.theme.largeRelayIconModifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.warningColor
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DMRelayListView(
onClose: () -> Unit,
accountViewModel: AccountViewModel,
relayToAdd: String = "",
nav: (String) -> Unit,
) {
val postViewModel: DMRelayListViewModel = viewModel()
val feedState by postViewModel.relays.collectAsStateWithLifecycle()
LaunchedEffect(Unit) { postViewModel.load(accountViewModel.account) }
Dialog(
onDismissRequest = onClose,
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Spacer(modifier = StdHorzSpacer)
Text(stringResource(R.string.dm_relays_title))
SaveButton(
onPost = {
postViewModel.create()
onClose()
},
true,
)
}
},
navigationIcon = {
Spacer(modifier = StdHorzSpacer)
CloseButton(
onPress = {
postViewModel.clear()
onClose()
},
)
},
colors =
TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
),
)
},
) { pad ->
Column(
modifier =
Modifier.padding(
16.dp,
pad.calculateTopPadding(),
16.dp,
pad.calculateBottomPadding(),
),
verticalArrangement = Arrangement.SpaceAround,
) {
Card(modifier = MaterialTheme.colorScheme.imageModifier) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(id = R.string.dm_relays_not_found_editing),
)
Spacer(modifier = StdVertSpacer)
Text(
text = stringResource(id = R.string.dm_relays_not_found_examples),
)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
LazyColumn(
contentPadding = FeedPadding,
) {
itemsIndexed(feedState, key = { _, item -> item.url }) { index, item ->
DMServerConfig(
item,
onDelete = { postViewModel.deleteRelay(item) },
accountViewModel = accountViewModel,
) {
onClose()
nav(it)
}
}
}
}
Spacer(modifier = StdVertSpacer)
DMEditableServerConfig(relayToAdd) { postViewModel.addRelay(it) }
}
}
}
}
@Composable
fun DMServerConfig(
item: DMRelayListViewModel.DMRelaySetupInfo,
onDelete: (DMRelayListViewModel.DMRelaySetupInfo) -> Unit,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
var relayInfo: RelayInfoDialog? by remember { mutableStateOf(null) }
val context = LocalContext.current
relayInfo?.let {
RelayInformationDialog(
onClose = { relayInfo = null },
relayInfo = it.relayInfo,
relayBriefInfo = it.relayBriefInfo,
accountViewModel = accountViewModel,
nav = nav,
)
}
val automaticallyShowProfilePicture =
remember {
accountViewModel.settings.showProfilePictures.value
}
DMServerConfigClickableLine(
item = item,
loadProfilePicture = automaticallyShowProfilePicture,
onDelete = onDelete,
accountViewModel = accountViewModel,
onClick = {
accountViewModel.retrieveRelayDocument(
item.url,
onInfo = { relayInfo = RelayInfoDialog(RelayBriefInfoCache.RelayBriefInfo(item.url), it) },
onError = { url, errorCode, exceptionMessage ->
val msg =
when (errorCode) {
Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL ->
context.getString(
R.string.relay_information_document_error_assemble_url,
url,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER ->
context.getString(
R.string.relay_information_document_error_assemble_url,
url,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT ->
context.getString(
R.string.relay_information_document_error_assemble_url,
url,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS ->
context.getString(
R.string.relay_information_document_error_assemble_url,
url,
exceptionMessage,
)
}
accountViewModel.toast(
context.getString(R.string.unable_to_download_relay_document),
msg,
)
},
)
},
)
}
@Composable
fun DMServerConfigClickableLine(
item: DMRelayListViewModel.DMRelaySetupInfo,
loadProfilePicture: Boolean,
onDelete: (DMRelayListViewModel.DMRelaySetupInfo) -> Unit,
onClick: () -> Unit,
accountViewModel: AccountViewModel,
) {
Column(Modifier.fillMaxWidth()) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 5.dp),
) {
Column(Modifier.clickable(onClick = onClick)) {
val iconUrlFromRelayInfoDoc =
remember(item) {
Nip11CachedRetriever.getFromCache(item.url)?.icon
}
RenderRelayIcon(
item.briefInfo.displayUrl,
iconUrlFromRelayInfoDoc ?: item.briefInfo.favIcon,
loadProfilePicture,
MaterialTheme.colorScheme.largeRelayIconModifier,
)
}
Spacer(modifier = HalfHorzPadding)
Column(Modifier.weight(1f)) {
FirstLine(item, onClick, onDelete, ReactionRowHeightChat.fillMaxWidth())
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = ReactionRowHeightChat.fillMaxWidth(),
) {
RenderStatusRow(
item = item,
modifier = HalfStartPadding.weight(1f),
accountViewModel = accountViewModel,
)
}
}
}
HorizontalDivider(thickness = DividerThickness)
}
}
@Composable
@OptIn(ExperimentalFoundationApi::class)
private fun RenderStatusRow(
item: DMRelayListViewModel.DMRelaySetupInfo,
modifier: Modifier,
accountViewModel: AccountViewModel,
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
Icon(
imageVector = Icons.Default.Download,
contentDescription = stringResource(R.string.read_from_relay),
modifier =
Modifier
.size(15.dp)
.combinedClickable(
onClick = {},
onLongClick = {
accountViewModel.toast(
R.string.read_from_relay,
R.string.read_from_relay_description,
)
},
),
tint = MaterialTheme.colorScheme.allGoodColor,
)
Text(
text = countToHumanReadableBytes(item.downloadCountInBytes),
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
color = MaterialTheme.colorScheme.placeholderText,
)
Icon(
imageVector = Icons.Default.Upload,
stringResource(R.string.write_to_relay),
modifier =
Modifier
.size(15.dp)
.combinedClickable(
onClick = { },
onLongClick = {
accountViewModel.toast(
R.string.write_to_relay,
R.string.write_to_relay_description,
)
},
),
tint = MaterialTheme.colorScheme.allGoodColor,
)
Text(
text = countToHumanReadableBytes(item.uploadCountInBytes),
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
color = MaterialTheme.colorScheme.placeholderText,
)
Icon(
imageVector = Icons.Default.SyncProblem,
stringResource(R.string.errors),
modifier =
Modifier
.size(15.dp)
.combinedClickable(
onClick = {},
onLongClick = {
accountViewModel.toast(
R.string.errors,
R.string.errors_description,
)
},
),
tint =
if (item.errorCount > 0) {
MaterialTheme.colorScheme.warningColor
} else {
MaterialTheme.colorScheme.allGoodColor
},
)
Text(
text = countToHumanReadable(item.errorCount, "errors"),
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
color = MaterialTheme.colorScheme.placeholderText,
)
Icon(
imageVector = Icons.Default.DeleteSweep,
stringResource(R.string.spam),
modifier =
Modifier
.size(15.dp)
.combinedClickable(
onClick = {},
onLongClick = {
accountViewModel.toast(
R.string.spam,
R.string.spam_description,
)
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.spam),
Toast.LENGTH_SHORT,
)
.show()
}
},
),
tint =
if (item.spamCount > 0) {
MaterialTheme.colorScheme.warningColor
} else {
MaterialTheme.colorScheme.allGoodColor
},
)
Text(
text = countToHumanReadable(item.spamCount, "spam"),
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
color = MaterialTheme.colorScheme.placeholderText,
)
}
@Composable
private fun FirstLine(
item: DMRelayListViewModel.DMRelaySetupInfo,
onClick: () -> Unit,
onDelete: (DMRelayListViewModel.DMRelaySetupInfo) -> Unit,
modifier: Modifier,
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) {
Text(
text = item.briefInfo.displayUrl,
modifier = Modifier.clickable(onClick = onClick),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (item.paidRelay) {
Icon(
imageVector = Icons.Default.Paid,
null,
modifier =
Modifier
.padding(start = 5.dp, top = 1.dp)
.size(14.dp),
tint = MaterialTheme.colorScheme.allGoodColor,
)
}
}
IconButton(
modifier = Modifier.size(30.dp),
onClick = { onDelete(item) },
) {
Icon(
imageVector = Icons.Default.Cancel,
contentDescription = stringResource(id = R.string.remove),
modifier =
Modifier
.padding(start = 10.dp)
.size(15.dp),
tint = WarningColor,
)
}
}
}
@Composable
fun DMEditableServerConfig(
relayToAdd: String,
onNewRelay: (DMRelayListViewModel.DMRelaySetupInfo) -> Unit,
) {
var url by remember { mutableStateOf<String>(relayToAdd) }
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(Size10dp)) {
OutlinedTextField(
label = { Text(text = stringResource(R.string.add_a_relay)) },
modifier = Modifier.weight(1f),
value = url,
onValueChange = { url = it },
placeholder = {
Text(
text = "server.com",
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1,
)
},
singleLine = true,
)
Button(
onClick = {
if (url.isNotBlank() && url != "/") {
var addedWSS =
if (!url.startsWith("wss://") && !url.startsWith("ws://")) {
if (url.endsWith(".onion") || url.endsWith(".onion/")) {
"ws://$url"
} else {
"wss://$url"
}
} else {
url
}
if (url.endsWith("/")) addedWSS = addedWSS.dropLast(1)
onNewRelay(DMRelayListViewModel.DMRelaySetupInfo(addedWSS))
url = ""
}
},
shape = ButtonBorder,
colors =
ButtonDefaults.buttonColors(
containerColor =
if (url.isNotBlank()) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.placeholderText
},
),
) {
Text(text = stringResource(id = R.string.add), color = Color.White)
}
}
}
@@ -0,0 +1,123 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.relays.RelayPool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class DMRelayListViewModel : ViewModel() {
private lateinit var account: Account
private val _relays = MutableStateFlow<List<DMRelaySetupInfo>>(emptyList())
val relays = _relays.asStateFlow()
fun load(account: Account) {
this.account = account
clear()
loadRelayDocuments()
}
fun create() {
viewModelScope.launch(Dispatchers.IO) {
account.saveDMRelayList(_relays.value.map { it.url })
clear()
}
}
fun loadRelayDocuments() {
viewModelScope.launch(Dispatchers.IO) {
_relays.value.forEach { item ->
Nip11CachedRetriever.loadRelayInfo(
dirtyUrl = item.url,
onInfo = {
togglePaidRelay(item, it.limitation?.payment_required ?: false)
},
onError = { url, errorCode, exceptionMessage -> },
)
}
}
}
@Immutable
data class DMRelaySetupInfo(
val url: String,
val errorCount: Int = 0,
val downloadCountInBytes: Int = 0,
val uploadCountInBytes: Int = 0,
val spamCount: Int = 0,
val paidRelay: Boolean = false,
) {
val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url)
}
fun clear() {
_relays.update {
val relayList = account.getDMRelayList()?.relays() ?: emptyList()
relayList.map { relayUrl ->
val liveRelay = RelayPool.getRelay(relayUrl)
val errorCounter = liveRelay?.errorCounter ?: 0
val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0
val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0
val spamCounter = liveRelay?.spamCounter ?: 0
DMRelaySetupInfo(
relayUrl,
errorCounter,
eventDownloadCounter,
eventUploadCounter,
spamCounter,
)
}.distinctBy { it.url }.sortedBy { it.downloadCountInBytes }.reversed()
}
}
fun addRelay(relay: DMRelaySetupInfo) {
if (relays.value.any { it.url == relay.url }) return
_relays.update { it.plus(relay) }
}
fun deleteRelay(relay: DMRelaySetupInfo) {
_relays.update { it.minus(relay) }
}
fun deleteAll() {
_relays.update { relays -> emptyList() }
}
fun togglePaidRelay(
relay: DMRelaySetupInfo,
paid: Boolean,
) {
_relays.update { it.updated(relay, relay.copy(paidRelay = paid)) }
}
}
@@ -20,52 +20,45 @@
*/
package com.vitorpamplona.amethyst.ui.note.elements
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ThemeType
import com.vitorpamplona.amethyst.ui.note.CloseIcon
import com.vitorpamplona.amethyst.ui.actions.DMRelayListView
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.BigPadding
import com.vitorpamplona.amethyst.ui.theme.StdPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import fr.acinq.secp256k1.Hex
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.runBlocking
@Preview
@Composable
@@ -89,85 +82,93 @@ fun AddInboxRelayForDMCardPreview() {
scope = myCoroutineScope,
)
runBlocking(Dispatchers.IO) {
val createAt = TimeUtils.now()
val list = listOf("wss://inbox.nostr.wine", "wss://vitor.nostr1.com")
val tags = ChatMessageRelayListEvent.createTagArray(list)
val id = "ab"
LocalCache.justConsume(
ChatMessageRelayListEvent(
id = id,
pubKey = pubkey,
createdAt = createAt,
tags = tags,
content = "",
sig = "",
),
null,
)
}
val accountViewModel =
AccountViewModel(
myAccount,
sharedPreferencesViewModel.sharedPrefs,
)
ThemeComparisonColumn {
AddInboxRelayForDMCard(
accountViewModel = accountViewModel,
nav = {},
)
}
}
@Composable
fun ObserveRelayListForDMsAndDisplayIfNotFound(
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
ObserveRelayListForDMs(
accountViewModel = accountViewModel,
) { relayListEvent ->
if (relayListEvent == null) {
AddInboxRelayForDMCard(
accountViewModel = accountViewModel,
nav = nav,
)
}
}
}
@Composable
fun ObserveRelayListForDMs(
accountViewModel: AccountViewModel,
inner: @Composable (relayListEvent: ChatMessageRelayListEvent?) -> Unit,
) {
ObserveRelayListForDMs(
pubkey = accountViewModel.account.userProfile().pubkeyHex,
accountViewModel = accountViewModel,
) { relayListEvent ->
inner(relayListEvent)
}
}
@Composable
fun ObserveRelayListForDMs(
pubkey: HexKey,
accountViewModel: AccountViewModel,
inner: @Composable (relayListEvent: ChatMessageRelayListEvent?) -> Unit,
) {
println("AABBCC ObserveRelayListForDMs $pubkey")
LoadAddressableNote(
ChatMessageRelayListEvent.createAddressTag("989c3734c46abac7ce3ce229971581a5a6ee39cdd6aa7261a55823fa7f8c4799"),
ChatMessageRelayListEvent.createAddressTag(pubkey),
accountViewModel,
) { relayList ->
Text("Test" + relayList)
if (relayList != null) {
ThemeComparisonColumn {
AddInboxRelayForDMCard(
relayList,
accountViewModel,
nav = {},
)
}
val relayListNoteState by relayList.live().metadata.observeAsState()
val relayListEvent = relayListNoteState?.note?.event as? ChatMessageRelayListEvent
println("AABBCC ObserveRelayListForDMs Event $relayListEvent ${relayListNoteState?.note?.idHex}")
inner(relayListEvent)
}
}
}
@Composable
fun AddInboxRelayForDMCard(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val releaseNoteState by baseNote.live().metadata.observeAsState()
val releaseNote = releaseNoteState?.note ?: return
Column(modifier = Modifier.padding(horizontal = Size10dp)) {
Column(modifier = StdPadding) {
Card(
modifier = MaterialTheme.colorScheme.imageModifier,
) {
Column(
modifier = Modifier.padding(16.dp),
modifier = BigPadding,
) {
// Title
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = stringResource(id = R.string.dm_relays_not_found),
style =
TextStyle(
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
),
)
IconButton(
modifier = Size20Modifier,
onClick = { accountViewModel.markDonatedInThisVersion() },
) {
CloseIcon()
}
}
Text(
text = stringResource(id = R.string.dm_relays_not_found),
style =
TextStyle(
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
),
)
Spacer(modifier = StdVertSpacer)
@@ -177,8 +178,20 @@ fun AddInboxRelayForDMCard(
Spacer(modifier = StdVertSpacer)
Text(
text = stringResource(id = R.string.dm_relays_not_found_examples),
)
Spacer(modifier = StdVertSpacer)
var wantsToEditRelays by remember { mutableStateOf(false) }
if (wantsToEditRelays) {
DMRelayListView({ wantsToEditRelays = false }, accountViewModel, nav = nav)
}
Button(
onClick = {
wantsToEditRelays = true
},
modifier = Modifier.fillMaxWidth(),
) {
@@ -186,7 +199,5 @@ fun AddInboxRelayForDMCard(
}
}
}
Spacer(modifier = DoubleVertSpacer)
}
}
@@ -104,6 +104,8 @@ import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures
import com.vitorpamplona.amethyst.ui.note.QuickActionAlertDialog
import com.vitorpamplona.amethyst.ui.note.UserCompose
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.note.elements.ObserveRelayListForDMs
import com.vitorpamplona.amethyst.ui.note.elements.ObserveRelayListForDMsAndDisplayIfNotFound
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
@@ -116,6 +118,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size34dp
import com.vitorpamplona.amethyst.ui.theme.StdPadding
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
import com.vitorpamplona.quartz.events.ChatMessageEvent
import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.findURLs
@@ -295,8 +298,14 @@ fun ChatroomScreen(
Column(Modifier.fillMaxHeight()) {
val replyTo = remember { mutableStateOf<Note?>(null) }
ObserveRelayListForDMsAndDisplayIfNotFound(accountViewModel, nav)
Column(
modifier = Modifier.fillMaxHeight().padding(vertical = 0.dp).weight(1f, true),
modifier =
Modifier
.fillMaxHeight()
.padding(vertical = 0.dp)
.weight(1f, true),
) {
RefreshingChatroomFeedView(
viewModel = feedViewModel,
@@ -425,7 +434,10 @@ fun PrivateMessageEditFieldRow(
UploadFromGallery(
isUploading = channelScreenModel.isUploadingImage,
tint = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.size(30.dp).padding(start = 2.dp),
modifier =
Modifier
.size(30.dp)
.padding(start = 2.dp),
) {
channelScreenModel.upload(
galleryUri = it,
@@ -603,7 +615,8 @@ fun ChatroomHeader(
) {
Column(
modifier =
Modifier.fillMaxWidth()
Modifier
.fillMaxWidth()
.clickable(
onClick = onClick,
),
@@ -635,7 +648,10 @@ fun GroupChatroomHeader(
onClick: () -> Unit,
) {
Column(
modifier = Modifier.fillMaxWidth().clickable(onClick = onClick),
modifier =
Modifier
.fillMaxWidth()
.clickable(onClick = onClick),
) {
Column(
verticalArrangement = Arrangement.Center,
@@ -669,7 +685,10 @@ private fun EditRoomSubjectButton(
}
Button(
modifier = Modifier.padding(horizontal = 3.dp).width(50.dp),
modifier =
Modifier
.padding(horizontal = 3.dp)
.width(50.dp),
onClick = { wantsToPost = true },
contentPadding = ZeroPadding,
) {
@@ -703,7 +722,10 @@ fun NewSubjectView(
val scope = rememberCoroutineScope()
Column(
modifier = Modifier.padding(10.dp).verticalScroll(rememberScrollState()),
modifier =
Modifier
.padding(10.dp)
.verticalScroll(rememberScrollState()),
) {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -755,7 +777,10 @@ fun NewSubjectView(
OutlinedTextField(
label = { Text(text = stringResource(R.string.messages_new_subject_message)) },
modifier = Modifier.fillMaxWidth().height(100.dp),
modifier =
Modifier
.fillMaxWidth()
.height(100.dp),
value = message.value,
onValueChange = { message.value = it },
placeholder = {
@@ -824,6 +849,31 @@ fun LongRoomHeader(
}
}
@Composable
fun DMRelayLine(
baseUser: User,
accountViewModel: AccountViewModel,
) {
ObserveRelayListForDMs(pubkey = baseUser.pubkeyHex, accountViewModel = accountViewModel) {
val relayList = it?.relays()
if (relayList.isNullOrEmpty()) {
Text(
text = stringResource(id = R.string.dm_relays_regular),
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
} else {
Text(
text = stringResource(id = R.string.dm_relays_through, relayList.joinToString(", ") { RelayUrlFormatter.displayUrl(it) }),
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
@Composable
fun RoomNameOnlyDisplay(
room: ChatroomKey,