Merge pull request #462 from greenart7c3/relay_information

Relay information dialog
This commit is contained in:
Vitor Pamplona
2023-06-21 14:03:34 -04:00
committed by GitHub
9 changed files with 817 additions and 75 deletions
@@ -0,0 +1,250 @@
package com.vitorpamplona.amethyst.model
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import java.lang.reflect.Type
class RelayInformation(
val name: String?,
val description: String?,
val pubkey: String?,
val contact: String?,
val supported_nips: List<Int>?,
val supported_nip_extensions: List<String>?,
val software: String?,
val version: String?,
val limitation: RelayInformationLimitation?,
val relay_countries: List<String>?,
val language_tags: List<String>?,
val tags: List<String>?,
val posting_policy: String?,
val payments_url: String?,
val fees: RelayInformationFees?
) {
companion object {
val gson: Gson = GsonBuilder()
.disableHtmlEscaping()
.registerTypeAdapter(RelayInformation::class.java, RelayInformationSerializer())
.registerTypeAdapter(RelayInformationLimitation::class.java, RelayInformationLimitationSerializer())
.registerTypeAdapter(RelayInformationFees::class.java, RelayInformationFeesSerializer())
.registerTypeAdapter(RelayInformationFee::class.java, RelayInformationFeeSerializer())
.create()
fun fromJson(json: String): RelayInformation = gson.fromJson(json, RelayInformation::class.java)
}
}
class RelayInformationFee(
val amount: Int?,
val unit: String?,
val period: Int?,
val kinds: List<Int>?
) {
companion object {
val gson: Gson = GsonBuilder()
.disableHtmlEscaping()
.registerTypeAdapter(RelayInformationFee::class.java, RelayInformationFeeSerializer())
.create()
fun fromJson(json: String): RelayInformationFee = gson.fromJson(json, RelayInformationFee::class.java)
}
}
private class RelayInformationFeeSerializer : JsonSerializer<RelayInformationFee> {
override fun serialize(
src: RelayInformationFee,
typeOfSrc: Type?,
context: JsonSerializationContext?
): JsonElement {
return JsonObject().apply {
addProperty("amount", src.amount)
addProperty("unit", src.unit)
addProperty("period", src.period)
add(
"kinds",
JsonArray().also { kinds ->
src.kinds?.forEach { kind ->
kinds.add(
kind
)
}
}
)
}
}
}
class RelayInformationFees(
val admission: List<RelayInformationFee>?,
val subscription: List<RelayInformationFee>?,
val publication: List<RelayInformationFee>?
) {
companion object {
val gson: Gson = GsonBuilder()
.disableHtmlEscaping()
.registerTypeAdapter(RelayInformationFees::class.java, RelayInformationFeesSerializer())
.create()
fun fromJson(json: String): RelayInformationFees = gson.fromJson(json, RelayInformationFees::class.java)
}
}
private class RelayInformationFeesSerializer : JsonSerializer<RelayInformationFees> {
override fun serialize(
src: RelayInformationFees,
typeOfSrc: Type?,
context: JsonSerializationContext?
): JsonElement {
return JsonObject().apply {
add(
"admission",
JsonArray().also { admissions ->
src.admission?.forEach { admission ->
admissions.add(
admission.toString()
)
}
}
)
add(
"publication",
JsonArray().also { publications ->
src.publication?.forEach { publication ->
publications.add(
publication.toString()
)
}
}
)
add(
"subscription",
JsonArray().also { subscriptions ->
src.subscription?.forEach { subscription ->
subscriptions.add(
subscription.toString()
)
}
}
)
}
}
}
class RelayInformationLimitation(
val max_message_length: Int?,
val max_subscriptions: Int?,
val max_filters: Int?,
val max_limit: Int?,
val max_subid_length: Int?,
val min_prefix: Int?,
val max_event_tags: Int?,
val max_content_length: Int?,
val min_pow_difficulty: Int?,
val auth_required: Boolean?,
val payment_required: Boolean?
) {
companion object {
val gson: Gson = GsonBuilder()
.disableHtmlEscaping()
.registerTypeAdapter(RelayInformationLimitation::class.java, RelayInformationLimitationSerializer())
.create()
fun fromJson(json: String): RelayInformationLimitation = gson.fromJson(json, RelayInformationLimitation::class.java)
}
}
private class RelayInformationLimitationSerializer : JsonSerializer<RelayInformationLimitation> {
override fun serialize(
src: RelayInformationLimitation,
typeOfSrc: Type?,
context: JsonSerializationContext?
): JsonElement {
return JsonObject().apply {
addProperty("max_message_length", src.max_message_length)
addProperty("max_subscriptions", src.max_subscriptions)
addProperty("max_filters", src.max_filters)
addProperty("max_limit", src.max_limit)
addProperty("max_subid_length", src.max_subid_length)
addProperty("min_prefix", src.min_prefix)
addProperty("max_event_tags", src.max_event_tags)
addProperty("max_content_length", src.max_content_length)
addProperty("min_pow_difficulty", src.min_pow_difficulty)
addProperty("auth_required", src.auth_required)
addProperty("payment_required", src.payment_required)
}
}
}
private class RelayInformationSerializer : JsonSerializer<RelayInformation> {
override fun serialize(
src: RelayInformation,
typeOfSrc: Type?,
context: JsonSerializationContext?
): JsonElement {
return JsonObject().apply {
addProperty("name", src.name)
addProperty("description", src.description)
addProperty("pubkey", src.pubkey)
addProperty("contact", src.contact)
add(
"supported_nip_extensions",
JsonArray().also { supported_nip_extensions ->
src.supported_nip_extensions?.forEach { nip ->
supported_nip_extensions.add(
nip
)
}
}
)
add(
"supported_nips",
JsonArray().also { supported_nips ->
src.supported_nips?.forEach { nip ->
supported_nips.add(
nip
)
}
}
)
addProperty("software", src.software)
addProperty("version", src.version)
add(
"relay_countries",
JsonArray().also { relay_countries ->
src.relay_countries?.forEach { country ->
relay_countries.add(
country
)
}
}
)
add(
"language_tags",
JsonArray().also { language_tags ->
src.language_tags?.forEach { language_tag ->
language_tags.add(
language_tag
)
}
}
)
add(
"tags",
JsonArray().also { tags ->
src.tags?.forEach { tag ->
tags.add(
tag
)
}
}
)
addProperty("posting_policy", src.posting_policy)
addProperty("payments_url", src.payments_url)
}
}
}
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.actions
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -53,17 +54,25 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.RelayInformation
import com.vitorpamplona.amethyst.model.RelaySetupInfo import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Request
import okhttp3.Response
import java.lang.Math.round import java.lang.Math.round
@Composable @Composable
fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, relayToAdd: String = "") { fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, relayToAdd: String = "", nav: (String) -> Unit) {
val postViewModel: NewRelayListViewModel = viewModel() val postViewModel: NewRelayListViewModel = viewModel()
val feedState by postViewModel.relays.collectAsState() val feedState by postViewModel.relays.collectAsState()
@@ -125,7 +134,9 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re
onToggleGlobal = { postViewModel.toggleGlobal(it) }, onToggleGlobal = { postViewModel.toggleGlobal(it) },
onToggleSearch = { postViewModel.toggleSearch(it) }, onToggleSearch = { postViewModel.toggleSearch(it) },
onDelete = { postViewModel.deleteRelay(it) } onDelete = { postViewModel.deleteRelay(it) },
accountViewModel = accountViewModel,
nav = nav
) )
} }
} }
@@ -222,10 +233,31 @@ fun ServerConfig(
onToggleGlobal: (RelaySetupInfo) -> Unit, onToggleGlobal: (RelaySetupInfo) -> Unit,
onToggleSearch: (RelaySetupInfo) -> Unit, onToggleSearch: (RelaySetupInfo) -> Unit,
onDelete: (RelaySetupInfo) -> Unit onDelete: (RelaySetupInfo) -> Unit,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var relayInfo: RelayInformation? by remember { mutableStateOf(null) }
if (relayInfo != null) {
val user = LocalCache.getOrCreateUser(relayInfo!!.pubkey ?: "")
NostrUserProfileDataSource.loadUserProfile(user)
NostrUserProfileDataSource.start()
RelayInformationDialog(
onClose = {
relayInfo = null
NostrUserProfileDataSource.loadUserProfile(null)
NostrUserProfileDataSource.stop()
},
relayInfo = relayInfo!!,
user,
accountViewModel,
nav
)
}
Column(Modifier.fillMaxWidth()) { Column(Modifier.fillMaxWidth()) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -251,7 +283,52 @@ fun ServerConfig(
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
text = item.url.removePrefix("wss://"), text = item.url.removePrefix("wss://"),
modifier = Modifier.weight(1f), modifier = Modifier
.weight(1f)
.clickable {
val client = HttpClient.getHttpClient()
val url = item.url
.replace("wss://", "https://")
.replace("ws://", "http://")
val request: Request = Request
.Builder()
.header("Accept", "application/nostr+json")
.url(url)
.build()
client
.newCall(request)
.enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
response.use {
if (it.isSuccessful) {
relayInfo =
RelayInformation.fromJson(it.body.string())
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.an_error_ocurred_trying_to_get_relay_information),
Toast.LENGTH_SHORT
).show()
}
}
}
}
override fun onFailure(call: Call, e: java.io.IOException) {
e.printStackTrace()
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.an_error_ocurred_trying_to_get_relay_information),
Toast.LENGTH_SHORT
).show()
}
}
})
},
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
@@ -275,11 +352,13 @@ fun ServerConfig(
onClick = { onToggleFollows(item) }, onClick = { onToggleFollows(item) },
onLongClick = { onLongClick = {
scope.launch { scope.launch {
Toast.makeText( Toast
context, .makeText(
context.getString(R.string.home_feed), context,
Toast.LENGTH_SHORT context.getString(R.string.home_feed),
).show() Toast.LENGTH_SHORT
)
.show()
} }
} }
), ),
@@ -306,11 +385,13 @@ fun ServerConfig(
onClick = { onTogglePrivateDMs(item) }, onClick = { onTogglePrivateDMs(item) },
onLongClick = { onLongClick = {
scope.launch { scope.launch {
Toast.makeText( Toast
context, .makeText(
context.getString(R.string.private_message_feed), context,
Toast.LENGTH_SHORT context.getString(R.string.private_message_feed),
).show() Toast.LENGTH_SHORT
)
.show()
} }
} }
), ),
@@ -337,11 +418,13 @@ fun ServerConfig(
onClick = { onTogglePublicChats(item) }, onClick = { onTogglePublicChats(item) },
onLongClick = { onLongClick = {
scope.launch { scope.launch {
Toast.makeText( Toast
context, .makeText(
context.getString(R.string.public_chat_feed), context,
Toast.LENGTH_SHORT context.getString(R.string.public_chat_feed),
).show() Toast.LENGTH_SHORT
)
.show()
} }
} }
), ),
@@ -368,11 +451,13 @@ fun ServerConfig(
onClick = { onToggleGlobal(item) }, onClick = { onToggleGlobal(item) },
onLongClick = { onLongClick = {
scope.launch { scope.launch {
Toast.makeText( Toast
context, .makeText(
context.getString(R.string.global_feed), context,
Toast.LENGTH_SHORT context.getString(R.string.global_feed),
).show() Toast.LENGTH_SHORT
)
.show()
} }
} }
), ),
@@ -400,11 +485,13 @@ fun ServerConfig(
onClick = { onToggleSearch(item) }, onClick = { onToggleSearch(item) },
onLongClick = { onLongClick = {
scope.launch { scope.launch {
Toast.makeText( Toast
context, .makeText(
context.getString(R.string.search_feed), context,
Toast.LENGTH_SHORT context.getString(R.string.search_feed),
).show() Toast.LENGTH_SHORT
)
.show()
} }
} }
), ),
@@ -436,11 +523,13 @@ fun ServerConfig(
onClick = { onToggleDownload(item) }, onClick = { onToggleDownload(item) },
onLongClick = { onLongClick = {
scope.launch { scope.launch {
Toast.makeText( Toast
context, .makeText(
context.getString(R.string.read_from_relay), context,
Toast.LENGTH_SHORT context.getString(R.string.read_from_relay),
).show() Toast.LENGTH_SHORT
)
.show()
} }
} }
), ),
@@ -476,11 +565,13 @@ fun ServerConfig(
onClick = { onToggleUpload(item) }, onClick = { onToggleUpload(item) },
onLongClick = { onLongClick = {
scope.launch { scope.launch {
Toast.makeText( Toast
context, .makeText(
context.getString(R.string.write_to_relay), context,
Toast.LENGTH_SHORT context.getString(R.string.write_to_relay),
).show() Toast.LENGTH_SHORT
)
.show()
} }
} }
), ),
@@ -512,11 +603,13 @@ fun ServerConfig(
onClick = { }, onClick = { },
onLongClick = { onLongClick = {
scope.launch { scope.launch {
Toast.makeText( Toast
context, .makeText(
context.getString(R.string.errors), context,
Toast.LENGTH_SHORT context.getString(R.string.errors),
).show() Toast.LENGTH_SHORT
)
.show()
} }
} }
), ),
@@ -541,11 +634,13 @@ fun ServerConfig(
onClick = { }, onClick = { },
onLongClick = { onLongClick = {
scope.launch { scope.launch {
Toast.makeText( Toast
context, .makeText(
context.getString(R.string.spam), context,
Toast.LENGTH_SHORT context.getString(R.string.spam),
).show() Toast.LENGTH_SHORT
)
.show()
} }
} }
), ),
@@ -0,0 +1,370 @@
package com.vitorpamplona.amethyst.ui.actions
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Downloading
import androidx.compose.material.icons.filled.Report
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
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 com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.RelayInformation
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.ClickableEmail
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.components.nip05VerificationAsAState
import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisplayLNAddress
import com.vitorpamplona.amethyst.ui.theme.Nip05
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
fun Section(text: String) {
Spacer(modifier = Modifier.height(10.dp))
Text(
text = text,
fontWeight = FontWeight.Bold,
fontSize = 25.sp
)
Spacer(modifier = Modifier.height(10.dp))
}
@Composable
fun SectionContent(text: String) {
Text(
modifier = Modifier.padding(start = 10.dp),
text = text
)
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun RelayInformationDialog(onClose: () -> Unit, relayInfo: RelayInformation, baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val userState by baseUser.live().metadata.observeAsState()
val user = remember(userState) { userState?.user } ?: return
val tags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() }
val lud16 = remember(userState) { user.info?.lud16?.trim() ?: user.info?.lud06?.trim() }
val pubkeyHex = remember { baseUser.pubkeyHex }
val uri = LocalUriHandler.current
val scrollState = rememberScrollState()
Dialog(
onDismissRequest = { onClose() },
properties = DialogProperties(
usePlatformDefaultWidth = false,
dismissOnClickOutside = false
)
) {
Surface {
Column(
modifier = Modifier
.padding(10.dp)
.fillMaxSize()
.verticalScroll(scrollState)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
onClose()
})
}
Row(
modifier = Modifier.fillMaxWidth()
) {
Section(relayInfo.name ?: "")
}
SectionContent(relayInfo.description ?: "")
Section(stringResource(R.string.owner))
Row {
UserPicture(
baseUser = user,
accountViewModel = accountViewModel,
size = 100.dp,
modifier = Modifier.border(
3.dp,
MaterialTheme.colors.background,
CircleShape
),
onClick = {
nav("User/${user.pubkeyHex}")
}
)
Column(Modifier.padding(start = 10.dp)) {
(user.bestDisplayName() ?: user.bestUsername())?.let {
Row(verticalAlignment = Alignment.Bottom, modifier = Modifier.padding(top = 7.dp)) {
CreateTextWithEmoji(
text = it,
tags = tags,
fontWeight = FontWeight.Bold,
fontSize = 25.sp
)
}
}
if (user.bestDisplayName() != null) {
user.bestUsername()?.let {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp)
) {
CreateTextWithEmoji(
text = "@$it",
tags = tags,
color = MaterialTheme.colors.placeholderText
)
}
}
}
user.nip05()?.let { nip05 ->
if (nip05.split("@").size == 2) {
val nip05Verified by nip05VerificationAsAState(user.info!!, user.pubkeyHex)
Row(verticalAlignment = Alignment.CenterVertically) {
if (nip05Verified == null) {
Icon(
tint = Color.Yellow,
imageVector = Icons.Default.Downloading,
contentDescription = "Downloading",
modifier = Modifier.size(16.dp)
)
} else if (nip05Verified == true) {
Icon(
painter = painterResource(R.drawable.ic_verified),
"NIP-05 Verified",
tint = Nip05,
modifier = Modifier.size(16.dp)
)
} else {
Icon(
tint = Color.Red,
imageVector = Icons.Default.Report,
contentDescription = "Invalid Nip05",
modifier = Modifier.size(16.dp)
)
}
var domainPadStart = 5.dp
if (nip05.split("@")[0] != "_") {
Text(
text = AnnotatedString(nip05.split("@")[0] + "@"),
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
domainPadStart = 0.dp
}
ClickableText(
text = AnnotatedString(nip05.split("@")[1]),
onClick = { nip05.let { runCatching { uri.openUri("https://${it.split("@")[1]}") } } },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary),
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = domainPadStart),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
DisplayLNAddress(lud16, pubkeyHex, accountViewModel.account)
user.info?.about?.let {
Row(
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
) {
val defaultBackground = MaterialTheme.colors.background
val background = remember {
mutableStateOf(defaultBackground)
}
TranslatableRichTextViewer(
content = it,
canPreview = false,
tags = remember { ImmutableListOfLists(emptyList()) },
backgroundColor = background,
accountViewModel = accountViewModel,
nav = nav
)
}
}
}
}
Section(stringResource(R.string.software))
val url = (relayInfo.software ?: "").replace("git+", "")
Box(modifier = Modifier.padding(start = 10.dp)) {
ClickableUrl(
urlText = url,
url = url
)
}
Section(stringResource(R.string.version))
SectionContent(relayInfo.version ?: "")
Section(stringResource(R.string.contact))
Box(modifier = Modifier.padding(start = 10.dp)) {
ClickableEmail(relayInfo.contact ?: "")
}
Section(stringResource(R.string.supports))
FlowRow {
relayInfo.supported_nips?.forEach { item ->
val text = item.toString().padStart(2, '0')
Box(Modifier.padding(10.dp)) {
ClickableUrl(
urlText = "Nip-$text",
url = "https://github.com/nostr-protocol/nips/blob/master/$text.md"
)
}
}
relayInfo.supported_nip_extensions?.forEach { item ->
val text = item.padStart(2, '0')
Box(Modifier.padding(10.dp)) {
ClickableUrl(
urlText = "Nip-$text",
url = "https://github.com/nostr-protocol/nips/blob/master/$text.md"
)
}
}
}
relayInfo.fees?.admission?.let {
if (it.isNotEmpty()) {
Section(stringResource(R.string.admission_fees))
it.forEach { item ->
SectionContent("${item.amount?.div(1000) ?: 0} sats")
}
}
}
relayInfo.payments_url?.let {
Section(stringResource(R.string.payments_url))
Box(modifier = Modifier.padding(start = 10.dp)) {
ClickableUrl(
urlText = it,
url = it
)
}
}
relayInfo.limitation?.let {
Section(stringResource(R.string.limitations))
val authRequired = it.auth_required ?: false
val authRequiredText = if (authRequired) stringResource(R.string.yes) else stringResource(R.string.no)
val paymentRequired = it.payment_required ?: false
val paymentRequiredText = if (paymentRequired) stringResource(R.string.yes) else stringResource(R.string.no)
Column {
SectionContent("${stringResource(R.string.message_length)}: ${it.max_message_length ?: 0}")
SectionContent("${stringResource(R.string.subscriptions)}: ${it.max_subscriptions ?: 0}")
SectionContent("${stringResource(R.string.filters)}: ${it.max_subscriptions ?: 0}")
SectionContent("${stringResource(R.string.subscription_id_length)}: ${it.max_subid_length ?: 0}")
SectionContent("${stringResource(R.string.minimum_prefix)}: ${it.min_prefix ?: 0}")
SectionContent("${stringResource(R.string.maximum_event_tags)}: ${it.max_event_tags ?: 0}")
SectionContent("${stringResource(R.string.content_length)}: ${it.max_content_length ?: 0}")
SectionContent("${stringResource(R.string.minimum_pow)}: ${it.min_pow_difficulty ?: 0}")
SectionContent("${stringResource(R.string.auth)}: $authRequiredText")
SectionContent("${stringResource(R.string.payment)}: $paymentRequiredText")
}
}
relayInfo.relay_countries?.let {
Section(stringResource(R.string.countries))
FlowRow {
it.forEach { item ->
SectionContent(item)
}
}
}
relayInfo.language_tags?.let {
Section(stringResource(R.string.languages))
FlowRow {
it.forEach { item ->
SectionContent(item)
}
}
}
relayInfo.tags?.let {
Section(stringResource(R.string.tags))
FlowRow {
it.forEach { item ->
SectionContent(item)
}
}
}
relayInfo.posting_policy?.let {
Section(stringResource(R.string.posting_policy))
Box(Modifier.padding(10.dp)) {
ClickableUrl(
it,
it
)
}
}
}
}
}
}
@@ -95,7 +95,8 @@ fun AppTopBar(
followLists: FollowListViewModel, followLists: FollowListViewModel,
navEntryState: State<NavBackStackEntry?>, navEntryState: State<NavBackStackEntry?>,
scaffoldState: ScaffoldState, scaffoldState: ScaffoldState,
accountViewModel: AccountViewModel accountViewModel: AccountViewModel,
nav: (String) -> Unit
) { ) {
val currentRoute by remember(navEntryState.value) { val currentRoute by remember(navEntryState.value) {
derivedStateOf { derivedStateOf {
@@ -103,7 +104,7 @@ fun AppTopBar(
} }
} }
RenderTopRouteBar(currentRoute, followLists, scaffoldState, accountViewModel) RenderTopRouteBar(currentRoute, followLists, scaffoldState, accountViewModel, nav)
} }
@Composable @Composable
@@ -111,16 +112,17 @@ private fun RenderTopRouteBar(
currentRoute: String?, currentRoute: String?,
followLists: FollowListViewModel, followLists: FollowListViewModel,
scaffoldState: ScaffoldState, scaffoldState: ScaffoldState,
accountViewModel: AccountViewModel accountViewModel: AccountViewModel,
nav: (String) -> Unit
) { ) {
when (currentRoute) { when (currentRoute) {
Route.Channel.base -> NoTopBar() Route.Channel.base -> NoTopBar()
Route.Room.base -> NoTopBar() Route.Room.base -> NoTopBar()
// Route.Profile.route -> TopBarWithBackButton(nav) // Route.Profile.route -> TopBarWithBackButton(nav)
Route.Home.base -> HomeTopBar(followLists, scaffoldState, accountViewModel) Route.Home.base -> HomeTopBar(followLists, scaffoldState, accountViewModel, nav)
Route.Video.base -> StoriesTopBar(followLists, scaffoldState, accountViewModel) Route.Video.base -> StoriesTopBar(followLists, scaffoldState, accountViewModel, nav)
Route.Notification.base -> NotificationTopBar(followLists, scaffoldState, accountViewModel) Route.Notification.base -> NotificationTopBar(followLists, scaffoldState, accountViewModel, nav)
else -> MainTopBar(scaffoldState, accountViewModel) else -> MainTopBar(scaffoldState, accountViewModel, nav)
} }
} }
@@ -129,8 +131,8 @@ fun NoTopBar() {
} }
@Composable @Composable
fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) { fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
GenericTopBar(scaffoldState, accountViewModel) { accountViewModel -> GenericTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val list by remember(accountState) { val list by remember(accountState) {
@@ -150,8 +152,8 @@ fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState
} }
@Composable @Composable
fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) { fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
GenericTopBar(scaffoldState, accountViewModel) { accountViewModel -> GenericTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val list by remember(accountState) { val list by remember(accountState) {
@@ -171,8 +173,8 @@ fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, a
} }
@Composable @Composable
fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) { fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
GenericTopBar(scaffoldState, accountViewModel) { accountViewModel -> GenericTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val list by remember(accountState) { val list by remember(accountState) {
@@ -192,15 +194,15 @@ fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: Scaffold
} }
@Composable @Composable
fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) { fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
GenericTopBar(scaffoldState, accountViewModel) { GenericTopBar(scaffoldState, accountViewModel, nav) {
AmethystIcon() AmethystIcon()
} }
} }
@OptIn(coil.annotation.ExperimentalCoilApi::class) @OptIn(coil.annotation.ExperimentalCoilApi::class)
@Composable @Composable
fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, content: @Composable (AccountViewModel) -> Unit) { fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit, content: @Composable (AccountViewModel) -> Unit) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var wantsToEditRelays by remember { var wantsToEditRelays by remember {
@@ -208,7 +210,7 @@ fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewMod
} }
if (wantsToEditRelays) { if (wantsToEditRelays) {
NewRelayListView({ wantsToEditRelays = false }, accountViewModel) NewRelayListView({ wantsToEditRelays = false }, accountViewModel, nav = nav)
} }
Column(modifier = Modifier.height(50.dp)) { Column(modifier = Modifier.height(50.dp)) {
@@ -1199,7 +1199,7 @@ fun DisplayRelaySet(
) )
Column(modifier = Modifier.padding(start = 10.dp)) { Column(modifier = Modifier.padding(start = 10.dp)) {
RelayOptionsAction(relay, accountViewModel) RelayOptionsAction(relay, accountViewModel, nav)
} }
} }
} }
@@ -1234,7 +1234,8 @@ fun DisplayRelaySet(
@Composable @Composable
private fun RelayOptionsAction( private fun RelayOptionsAction(
relay: String, relay: String,
accountViewModel: AccountViewModel accountViewModel: AccountViewModel,
nav: (String) -> Unit
) { ) {
val userStateRelayInfo by accountViewModel.account.userProfile().live().relayInfo.observeAsState() val userStateRelayInfo by accountViewModel.account.userProfile().live().relayInfo.observeAsState()
val isCurrentlyOnTheUsersList by remember(userStateRelayInfo) { val isCurrentlyOnTheUsersList by remember(userStateRelayInfo) {
@@ -1248,7 +1249,7 @@ private fun RelayOptionsAction(
} }
if (wantsToAddRelay.isNotEmpty()) { if (wantsToAddRelay.isNotEmpty()) {
NewRelayListView({ wantsToAddRelay = "" }, accountViewModel, wantsToAddRelay) NewRelayListView({ wantsToAddRelay = "" }, accountViewModel, wantsToAddRelay, nav = nav)
} }
if (isCurrentlyOnTheUsersList) { if (isCurrentlyOnTheUsersList) {
@@ -98,6 +98,7 @@ class RelayFeedViewModel : ViewModel() {
fun RelayFeedView( fun RelayFeedView(
viewModel: RelayFeedViewModel, viewModel: RelayFeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit,
enablePullRefresh: Boolean = true enablePullRefresh: Boolean = true
) { ) {
val feedState by viewModel.feedContent.collectAsState() val feedState by viewModel.feedContent.collectAsState()
@@ -107,7 +108,7 @@ fun RelayFeedView(
} }
if (wantsToAddRelay.isNotEmpty()) { if (wantsToAddRelay.isNotEmpty()) {
NewRelayListView({ wantsToAddRelay = "" }, accountViewModel, wantsToAddRelay) NewRelayListView({ wantsToAddRelay = "" }, accountViewModel, wantsToAddRelay, nav = nav)
} }
var refreshing by remember { mutableStateOf(false) } var refreshing by remember { mutableStateOf(false) }
@@ -173,7 +173,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
AppBottomBar(accountViewModel, navState, navBottomRow) AppBottomBar(accountViewModel, navState, navBottomRow)
}, },
topBar = { topBar = {
AppTopBar(followLists, navState, scaffoldState, accountViewModel) AppTopBar(followLists, navState, scaffoldState, accountViewModel, nav = nav)
}, },
drawerContent = { drawerContent = {
DrawerContent(nav, scaffoldState, sheetState, accountViewModel) DrawerContent(nav, scaffoldState, sheetState, accountViewModel)
@@ -316,7 +316,7 @@ private fun CreateAndRenderPages(
4 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav) 4 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav)
5 -> TabBookmarks(baseUser, accountViewModel, nav) 5 -> TabBookmarks(baseUser, accountViewModel, nav)
6 -> TabReports(baseUser, accountViewModel, nav) 6 -> TabReports(baseUser, accountViewModel, nav)
7 -> TabRelays(baseUser, accountViewModel) 7 -> TabRelays(baseUser, accountViewModel, nav)
} }
} }
@@ -783,7 +783,7 @@ private fun DrawAdditionalInfo(
} }
@Composable @Composable
private fun DisplayLNAddress( fun DisplayLNAddress(
lud16: String?, lud16: String?,
userHex: String, userHex: String,
account: Account account: Account
@@ -1294,7 +1294,7 @@ private fun WatchReportsAndUpdateFeed(
} }
@Composable @Composable
fun TabRelays(user: User, accountViewModel: AccountViewModel) { fun TabRelays(user: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val feedViewModel: RelayFeedViewModel = viewModel() val feedViewModel: RelayFeedViewModel = viewModel()
val lifeCycleOwner = LocalLifecycleOwner.current val lifeCycleOwner = LocalLifecycleOwner.current
@@ -1323,7 +1323,7 @@ fun TabRelays(user: User, accountViewModel: AccountViewModel) {
Column( Column(
modifier = Modifier.padding(vertical = 0.dp) modifier = Modifier.padding(vertical = 0.dp)
) { ) {
RelayFeedView(feedViewModel, accountViewModel, enablePullRefresh = false) RelayFeedView(feedViewModel, accountViewModel, enablePullRefresh = false, nav = nav)
} }
} }
} }
+23
View File
@@ -425,4 +425,27 @@
<string name="sats_to_complete">Zapraiser at %1$s. %2$s sats to goal</string> <string name="sats_to_complete">Zapraiser at %1$s. %2$s sats to goal</string>
<string name="read_from_relay">Read from Relay</string> <string name="read_from_relay">Read from Relay</string>
<string name="write_to_relay">Write to Relay</string> <string name="write_to_relay">Write to Relay</string>
<string name="an_error_ocurred_trying_to_get_relay_information">An error ocurred trying to get relay information</string>
<string name="owner">Owner</string>
<string name="version">Version</string>
<string name="software">Software</string>
<string name="contact">Contact</string>
<string name="supports">Supports</string>
<string name="admission_fees">Admission Fees</string>
<string name="payments_url">Payments url</string>
<string name="limitations">Limitations</string>
<string name="countries">Countries</string>
<string name="languages">Languages</string>
<string name="tags">Tags</string>
<string name="posting_policy">Posting policy</string>
<string name="message_length">Message length</string>
<string name="subscriptions">Subscriptions</string>
<string name="filters">Filters</string>
<string name="subscription_id_length">Subscription id length</string>
<string name="minimum_prefix">Minimum prefix</string>
<string name="maximum_event_tags">Maximum event tags</string>
<string name="content_length">Content length</string>
<string name="minimum_pow">Minimum PoW</string>
<string name="auth">Auth</string>
<string name="payment">Payment</string>
</resources> </resources>