Improves relay list NIP-11 caching, updates NIP-11 properties in the document and adds icon rendering from the NIP11 document.

This commit is contained in:
Vitor Pamplona
2024-02-22 18:43:46 -05:00
parent 2ee4b24c9b
commit dd160ecb71
7 changed files with 119 additions and 30 deletions

View File

@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service
import android.util.Log import android.util.Log
import android.util.LruCache import android.util.LruCache
import com.vitorpamplona.quartz.encoders.Nip11RelayInformation import com.vitorpamplona.quartz.encoders.Nip11RelayInformation
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback import okhttp3.Callback
@@ -31,9 +32,21 @@ import okhttp3.Response
import java.io.IOException import java.io.IOException
object Nip11CachedRetriever { object Nip11CachedRetriever {
val relayInformationDocumentCache = LruCache<String, Nip11RelayInformation>(100) open class RetrieveResult(val time: Long)
class RetrieveResultError(val error: Nip11Retriever.ErrorCode, val msg: String? = null) : RetrieveResult(TimeUtils.now())
class RetrieveResultSuccess(val data: Nip11RelayInformation) : RetrieveResult(TimeUtils.now())
val relayInformationDocumentCache = LruCache<String, RetrieveResult?>(100)
val retriever = Nip11Retriever() val retriever = Nip11Retriever()
fun getFromCache(dirtyUrl: String): Nip11RelayInformation? {
val result = relayInformationDocumentCache.get(retriever.cleanUrl(dirtyUrl)) ?: return null
if (result is RetrieveResultSuccess) return result.data
return null
}
suspend fun loadRelayInfo( suspend fun loadRelayInfo(
dirtyUrl: String, dirtyUrl: String,
onInfo: (Nip11RelayInformation) -> Unit, onInfo: (Nip11RelayInformation) -> Unit,
@@ -43,17 +56,40 @@ object Nip11CachedRetriever {
val doc = relayInformationDocumentCache.get(url) val doc = relayInformationDocumentCache.get(url)
if (doc != null) { if (doc != null) {
onInfo(doc) if (doc is RetrieveResultSuccess) {
onInfo(doc.data)
} else if (doc is RetrieveResultError) {
if (TimeUtils.now() - doc.time < TimeUtils.ONE_HOUR) {
onError(dirtyUrl, doc.error, null)
} else {
Nip11Retriever()
.loadRelayInfo(
url = url,
dirtyUrl = dirtyUrl,
onInfo = {
relayInformationDocumentCache.put(url, RetrieveResultSuccess(it))
onInfo(it)
},
onError = { dirtyUrl, code, errorMsg ->
relayInformationDocumentCache.put(url, RetrieveResultError(code, errorMsg))
onError(url, code, errorMsg)
},
)
}
}
} else { } else {
Nip11Retriever() Nip11Retriever()
.loadRelayInfo( .loadRelayInfo(
url, url = url,
dirtyUrl, dirtyUrl = dirtyUrl,
onInfo = { onInfo = {
relayInformationDocumentCache.put(url, it) relayInformationDocumentCache.put(url, RetrieveResultSuccess(it))
onInfo(it) onInfo(it)
}, },
onError, onError = { dirtyUrl, code, errorMsg ->
relayInformationDocumentCache.put(url, RetrieveResultError(code, errorMsg))
onError(url, code, errorMsg)
},
) )
} }
} }
@@ -81,6 +117,7 @@ class Nip11Retriever {
onInfo: (Nip11RelayInformation) -> Unit, onInfo: (Nip11RelayInformation) -> Unit,
onError: (String, ErrorCode, String?) -> Unit, onError: (String, ErrorCode, String?) -> Unit,
) { ) {
checkNotInMainThread()
try { try {
val request: Request = val request: Request =
Request.Builder().header("Accept", "application/nostr+json").url(url).build() Request.Builder().header("Accept", "application/nostr+json").url(url).build()

View File

@@ -79,6 +79,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
import com.vitorpamplona.amethyst.model.RelaySetupInfo import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.Nip11Retriever import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.service.relays.Constants import com.vitorpamplona.amethyst.service.relays.Constants
import com.vitorpamplona.amethyst.service.relays.Constants.defaultRelays import com.vitorpamplona.amethyst.service.relays.Constants.defaultRelays
@@ -193,8 +194,10 @@ fun NewRelayListView(
onToggleSearch = { postViewModel.toggleSearch(it) }, onToggleSearch = { postViewModel.toggleSearch(it) },
onDelete = { postViewModel.deleteRelay(it) }, onDelete = { postViewModel.deleteRelay(it) },
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, ) {
) onClose()
nav(it)
}
} }
} }
} }
@@ -410,9 +413,14 @@ fun ServerConfigClickableLine(
modifier = Modifier.padding(vertical = 5.dp), modifier = Modifier.padding(vertical = 5.dp),
) { ) {
Column(Modifier.clickable(onClick = onClick)) { Column(Modifier.clickable(onClick = onClick)) {
val iconUrlFromRelayInfoDoc =
remember(item) {
Nip11CachedRetriever.getFromCache(item.url)?.icon
}
RenderRelayIcon( RenderRelayIcon(
item.briefInfo.displayUrl, item.briefInfo.displayUrl,
item.briefInfo.favIcon, iconUrlFromRelayInfoDoc ?: item.briefInfo.favIcon,
loadProfilePicture, loadProfilePicture,
MaterialTheme.colorScheme.largeRelayIconModifier, MaterialTheme.colorScheme.largeRelayIconModifier,
) )

View File

@@ -62,7 +62,9 @@ class NewRelayListViewModel : ViewModel() {
_relays.value.forEach { item -> _relays.value.forEach { item ->
Nip11CachedRetriever.loadRelayInfo( Nip11CachedRetriever.loadRelayInfo(
dirtyUrl = item.url, dirtyUrl = item.url,
onInfo = { togglePaidRelay(item, it.limitation?.payment_required ?: false) }, onInfo = {
togglePaidRelay(item, it.limitation?.payment_required ?: false)
},
onError = { url, errorCode, exceptionMessage -> }, onError = { url, errorCode, exceptionMessage -> },
) )
} }

View File

@@ -104,7 +104,7 @@ fun RelayInformationDialog(
Column { Column {
RenderRelayIcon( RenderRelayIcon(
relayBriefInfo.displayUrl, relayBriefInfo.displayUrl,
relayBriefInfo.favIcon, relayInfo.icon ?: relayBriefInfo.favIcon,
automaticallyShowProfilePicture, automaticallyShowProfilePicture,
MaterialTheme.colorScheme.largeRelayIconModifier, MaterialTheme.colorScheme.largeRelayIconModifier,
) )
@@ -121,7 +121,12 @@ fun RelayInformationDialog(
Section(stringResource(R.string.owner)) Section(stringResource(R.string.owner))
relayInfo.pubkey?.let { DisplayOwnerInformation(it, accountViewModel, nav) } relayInfo.pubkey?.let {
DisplayOwnerInformation(it, accountViewModel) {
onClose()
nav(it)
}
}
Section(stringResource(R.string.software)) Section(stringResource(R.string.software))
@@ -170,12 +175,14 @@ fun RelayInformationDialog(
relayInfo.limitation?.let { relayInfo.limitation?.let {
Section(stringResource(R.string.limitations)) Section(stringResource(R.string.limitations))
val authRequired = it.auth_required ?: false
val authRequiredText = val authRequiredText =
if (authRequired) stringResource(R.string.yes) else stringResource(R.string.no) if (it.auth_required ?: false) stringResource(R.string.yes) else stringResource(R.string.no)
val paymentRequired = it.payment_required ?: false
val paymentRequiredText = val paymentRequiredText =
if (paymentRequired) stringResource(R.string.yes) else stringResource(R.string.no) if (it.payment_required ?: false) stringResource(R.string.yes) else stringResource(R.string.no)
val restrictedWritesText =
if (it.restricted_writes ?: false) stringResource(R.string.yes) else stringResource(R.string.no)
Column { Column {
SectionContent( SectionContent(
@@ -184,7 +191,7 @@ fun RelayInformationDialog(
SectionContent( SectionContent(
"${stringResource(R.string.subscriptions)}: ${it.max_subscriptions ?: 0}", "${stringResource(R.string.subscriptions)}: ${it.max_subscriptions ?: 0}",
) )
SectionContent("${stringResource(R.string.filters)}: ${it.max_subscriptions ?: 0}") SectionContent("${stringResource(R.string.filters)}: ${it.max_filters ?: 0}")
SectionContent( SectionContent(
"${stringResource(R.string.subscription_id_length)}: ${it.max_subid_length ?: 0}", "${stringResource(R.string.subscription_id_length)}: ${it.max_subid_length ?: 0}",
) )
@@ -195,9 +202,13 @@ fun RelayInformationDialog(
SectionContent( SectionContent(
"${stringResource(R.string.content_length)}: ${it.max_content_length ?: 0}", "${stringResource(R.string.content_length)}: ${it.max_content_length ?: 0}",
) )
SectionContent(
"${stringResource(R.string.max_limit)}: ${it.max_limit ?: 0}",
)
SectionContent("${stringResource(R.string.minimum_pow)}: ${it.min_pow_difficulty ?: 0}") SectionContent("${stringResource(R.string.minimum_pow)}: ${it.min_pow_difficulty ?: 0}")
SectionContent("${stringResource(R.string.auth)}: $authRequiredText") SectionContent("${stringResource(R.string.auth)}: $authRequiredText")
SectionContent("${stringResource(R.string.payment)}: $paymentRequiredText") SectionContent("${stringResource(R.string.payment)}: $paymentRequiredText")
SectionContent("${stringResource(R.string.restricted_writes)}: $restrictedWritesText")
} }
} }

View File

@@ -39,6 +39,7 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
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
@@ -51,6 +52,7 @@ import androidx.lifecycle.map
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.model.RelayBriefInfoCache import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.Nip11Retriever import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.ui.actions.RelayInformationDialog import com.vitorpamplona.amethyst.ui.actions.RelayInformationDialog
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
@@ -61,7 +63,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size15dp
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.relayIconModifier import com.vitorpamplona.amethyst.ui.theme.relayIconModifier
import com.vitorpamplona.quartz.encoders.Nip11RelayInformation
@Composable @Composable
public fun RelayBadgesHorizontal( public fun RelayBadgesHorizontal(
@@ -132,11 +133,27 @@ fun RenderRelay(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
var relayInfo: Nip11RelayInformation? by remember { mutableStateOf(null) } val relayInfo by
produceState(
initialValue = Nip11CachedRetriever.getFromCache(relay.url),
) {
if (value == null) {
accountViewModel.retrieveRelayDocument(
relay.url,
onInfo = {
value = it
},
onError = { url, errorCode, exceptionMessage ->
},
)
}
}
if (relayInfo != null) { var openRelayDialog by remember { mutableStateOf(false) }
if (openRelayDialog && relayInfo != null) {
RelayInformationDialog( RelayInformationDialog(
onClose = { relayInfo = null }, onClose = { openRelayDialog = false },
relayInfo = relayInfo!!, relayInfo = relayInfo!!,
relayBriefInfo = relay, relayBriefInfo = relay,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
@@ -149,11 +166,6 @@ fun RenderRelay(
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val ripple = rememberRipple(bounded = false, radius = Size15dp) val ripple = rememberRipple(bounded = false, radius = Size15dp)
val automaticallyShowProfilePicture =
remember {
accountViewModel.settings.showProfilePictures.value
}
val clickableModifier = val clickableModifier =
remember(relay) { remember(relay) {
Modifier Modifier
@@ -166,7 +178,9 @@ fun RenderRelay(
onClick = { onClick = {
accountViewModel.retrieveRelayDocument( accountViewModel.retrieveRelayDocument(
relay.url, relay.url,
onInfo = { relayInfo = it }, onInfo = {
openRelayDialog = true
},
onError = { url, errorCode, exceptionMessage -> onError = { url, errorCode, exceptionMessage ->
val msg = val msg =
when (errorCode) { when (errorCode) {
@@ -213,14 +227,18 @@ fun RenderRelay(
modifier = clickableModifier, modifier = clickableModifier,
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
RenderRelayIcon(relay.displayUrl, relay.favIcon, automaticallyShowProfilePicture) RenderRelayIcon(
displayUrl = relay.displayUrl,
iconUrl = relayInfo?.icon ?: relay.favIcon,
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
)
} }
} }
@Composable @Composable
fun RenderRelayIcon( fun RenderRelayIcon(
displayUrl: String, displayUrl: String,
iconUrl: String, iconUrl: String?,
loadProfilePicture: Boolean, loadProfilePicture: Boolean,
iconModifier: Modifier = MaterialTheme.colorScheme.relayIconModifier, iconModifier: Modifier = MaterialTheme.colorScheme.relayIconModifier,
) { ) {

View File

@@ -774,4 +774,6 @@
<string name="this_version_brought_to_you_by">This version was brought to you by:</string> <string name="this_version_brought_to_you_by">This version was brought to you by:</string>
<string name="version_name">Version %1$s</string> <string name="version_name">Version %1$s</string>
<string name="thank_you">Thank you!</string> <string name="thank_you">Thank you!</string>
<string name="max_limit">Max Limit</string>
<string name="restricted_writes">Restricted Writes</string>
</resources> </resources>

View File

@@ -29,6 +29,7 @@ class Nip11RelayInformation(
val id: String?, val id: String?,
val name: String?, val name: String?,
val description: String?, val description: String?,
val icon: String?,
val pubkey: String?, val pubkey: String?,
val contact: String?, val contact: String?,
val supported_nips: List<Int>?, val supported_nips: List<Int>?,
@@ -41,7 +42,9 @@ class Nip11RelayInformation(
val tags: List<String>?, val tags: List<String>?,
val posting_policy: String?, val posting_policy: String?,
val payments_url: String?, val payments_url: String?,
val retention: List<RelayInformationRetentionData>?,
val fees: RelayInformationFees?, val fees: RelayInformationFees?,
val nip50: List<String>?,
) { ) {
companion object { companion object {
val mapper = val mapper =
@@ -63,7 +66,6 @@ class RelayInformationFees(
val admission: List<RelayInformationFee>?, val admission: List<RelayInformationFee>?,
val subscription: List<RelayInformationFee>?, val subscription: List<RelayInformationFee>?,
val publication: List<RelayInformationFee>?, val publication: List<RelayInformationFee>?,
val retention: List<RelayInformationFee>?,
) )
class RelayInformationLimitation( class RelayInformationLimitation(
@@ -78,4 +80,13 @@ class RelayInformationLimitation(
val min_pow_difficulty: Int?, val min_pow_difficulty: Int?,
val auth_required: Boolean?, val auth_required: Boolean?,
val payment_required: Boolean?, val payment_required: Boolean?,
val restricted_writes: Boolean?,
val created_at_lower_limit: Int?,
val created_at_upper_limit: Int?,
)
class RelayInformationRetentionData(
val kinds: ArrayList<Int>,
val tiem: Int?,
val count: Int?,
) )