mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-04-13 14:29:27 +02:00
Merge pull request #462 from greenart7c3/relay_information
Relay information dialog
This commit is contained in:
commit
54462f8d26
@ -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 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
|
||||
@ -53,17 +54,25 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
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.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import java.lang.Math.round
|
||||
|
||||
@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 feedState by postViewModel.relays.collectAsState()
|
||||
|
||||
@ -125,7 +134,9 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re
|
||||
onToggleGlobal = { postViewModel.toggleGlobal(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,
|
||||
onToggleSearch: (RelaySetupInfo) -> Unit,
|
||||
|
||||
onDelete: (RelaySetupInfo) -> Unit
|
||||
onDelete: (RelaySetupInfo) -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
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()) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@ -251,7 +283,52 @@ fun ServerConfig(
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
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,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
@ -275,11 +352,13 @@ fun ServerConfig(
|
||||
onClick = { onToggleFollows(item) },
|
||||
onLongClick = {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.home_feed),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.home_feed),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
),
|
||||
@ -306,11 +385,13 @@ fun ServerConfig(
|
||||
onClick = { onTogglePrivateDMs(item) },
|
||||
onLongClick = {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.private_message_feed),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.private_message_feed),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
),
|
||||
@ -337,11 +418,13 @@ fun ServerConfig(
|
||||
onClick = { onTogglePublicChats(item) },
|
||||
onLongClick = {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.public_chat_feed),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.public_chat_feed),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
),
|
||||
@ -368,11 +451,13 @@ fun ServerConfig(
|
||||
onClick = { onToggleGlobal(item) },
|
||||
onLongClick = {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.global_feed),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.global_feed),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
),
|
||||
@ -400,11 +485,13 @@ fun ServerConfig(
|
||||
onClick = { onToggleSearch(item) },
|
||||
onLongClick = {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.search_feed),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.search_feed),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
),
|
||||
@ -436,11 +523,13 @@ fun ServerConfig(
|
||||
onClick = { onToggleDownload(item) },
|
||||
onLongClick = {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.read_from_relay),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.read_from_relay),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
),
|
||||
@ -476,11 +565,13 @@ fun ServerConfig(
|
||||
onClick = { onToggleUpload(item) },
|
||||
onLongClick = {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.write_to_relay),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.write_to_relay),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
),
|
||||
@ -512,11 +603,13 @@ fun ServerConfig(
|
||||
onClick = { },
|
||||
onLongClick = {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.errors),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.errors),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
),
|
||||
@ -541,11 +634,13 @@ fun ServerConfig(
|
||||
onClick = { },
|
||||
onLongClick = {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.spam),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.spam),
|
||||
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,
|
||||
navEntryState: State<NavBackStackEntry?>,
|
||||
scaffoldState: ScaffoldState,
|
||||
accountViewModel: AccountViewModel
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val currentRoute by remember(navEntryState.value) {
|
||||
derivedStateOf {
|
||||
@ -103,7 +104,7 @@ fun AppTopBar(
|
||||
}
|
||||
}
|
||||
|
||||
RenderTopRouteBar(currentRoute, followLists, scaffoldState, accountViewModel)
|
||||
RenderTopRouteBar(currentRoute, followLists, scaffoldState, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ -111,16 +112,17 @@ private fun RenderTopRouteBar(
|
||||
currentRoute: String?,
|
||||
followLists: FollowListViewModel,
|
||||
scaffoldState: ScaffoldState,
|
||||
accountViewModel: AccountViewModel
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
when (currentRoute) {
|
||||
Route.Channel.base -> NoTopBar()
|
||||
Route.Room.base -> NoTopBar()
|
||||
// Route.Profile.route -> TopBarWithBackButton(nav)
|
||||
Route.Home.base -> HomeTopBar(followLists, scaffoldState, accountViewModel)
|
||||
Route.Video.base -> StoriesTopBar(followLists, scaffoldState, accountViewModel)
|
||||
Route.Notification.base -> NotificationTopBar(followLists, scaffoldState, accountViewModel)
|
||||
else -> MainTopBar(scaffoldState, accountViewModel)
|
||||
Route.Home.base -> HomeTopBar(followLists, scaffoldState, accountViewModel, nav)
|
||||
Route.Video.base -> StoriesTopBar(followLists, scaffoldState, accountViewModel, nav)
|
||||
Route.Notification.base -> NotificationTopBar(followLists, scaffoldState, accountViewModel, nav)
|
||||
else -> MainTopBar(scaffoldState, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,8 +131,8 @@ fun NoTopBar() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||
GenericTopBar(scaffoldState, accountViewModel) { accountViewModel ->
|
||||
fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
GenericTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
|
||||
val list by remember(accountState) {
|
||||
@ -150,8 +152,8 @@ fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||
GenericTopBar(scaffoldState, accountViewModel) { accountViewModel ->
|
||||
fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
GenericTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
|
||||
val list by remember(accountState) {
|
||||
@ -171,8 +173,8 @@ fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, a
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||
GenericTopBar(scaffoldState, accountViewModel) { accountViewModel ->
|
||||
fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
GenericTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
|
||||
val list by remember(accountState) {
|
||||
@ -192,15 +194,15 @@ fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: Scaffold
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||
GenericTopBar(scaffoldState, accountViewModel) {
|
||||
fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
GenericTopBar(scaffoldState, accountViewModel, nav) {
|
||||
AmethystIcon()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(coil.annotation.ExperimentalCoilApi::class)
|
||||
@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()
|
||||
|
||||
var wantsToEditRelays by remember {
|
||||
@ -208,7 +210,7 @@ fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewMod
|
||||
}
|
||||
|
||||
if (wantsToEditRelays) {
|
||||
NewRelayListView({ wantsToEditRelays = false }, accountViewModel)
|
||||
NewRelayListView({ wantsToEditRelays = false }, accountViewModel, nav = nav)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.height(50.dp)) {
|
||||
|
@ -1199,7 +1199,7 @@ fun DisplayRelaySet(
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
RelayOptionsAction(relay, accountViewModel)
|
||||
RelayOptionsAction(relay, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1234,7 +1234,8 @@ fun DisplayRelaySet(
|
||||
@Composable
|
||||
private fun RelayOptionsAction(
|
||||
relay: String,
|
||||
accountViewModel: AccountViewModel
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val userStateRelayInfo by accountViewModel.account.userProfile().live().relayInfo.observeAsState()
|
||||
val isCurrentlyOnTheUsersList by remember(userStateRelayInfo) {
|
||||
@ -1248,7 +1249,7 @@ private fun RelayOptionsAction(
|
||||
}
|
||||
|
||||
if (wantsToAddRelay.isNotEmpty()) {
|
||||
NewRelayListView({ wantsToAddRelay = "" }, accountViewModel, wantsToAddRelay)
|
||||
NewRelayListView({ wantsToAddRelay = "" }, accountViewModel, wantsToAddRelay, nav = nav)
|
||||
}
|
||||
|
||||
if (isCurrentlyOnTheUsersList) {
|
||||
|
@ -98,6 +98,7 @@ class RelayFeedViewModel : ViewModel() {
|
||||
fun RelayFeedView(
|
||||
viewModel: RelayFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
enablePullRefresh: Boolean = true
|
||||
) {
|
||||
val feedState by viewModel.feedContent.collectAsState()
|
||||
@ -107,7 +108,7 @@ fun RelayFeedView(
|
||||
}
|
||||
|
||||
if (wantsToAddRelay.isNotEmpty()) {
|
||||
NewRelayListView({ wantsToAddRelay = "" }, accountViewModel, wantsToAddRelay)
|
||||
NewRelayListView({ wantsToAddRelay = "" }, accountViewModel, wantsToAddRelay, nav = nav)
|
||||
}
|
||||
|
||||
var refreshing by remember { mutableStateOf(false) }
|
||||
|
@ -173,7 +173,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
||||
AppBottomBar(accountViewModel, navState, navBottomRow)
|
||||
},
|
||||
topBar = {
|
||||
AppTopBar(followLists, navState, scaffoldState, accountViewModel)
|
||||
AppTopBar(followLists, navState, scaffoldState, accountViewModel, nav = nav)
|
||||
},
|
||||
drawerContent = {
|
||||
DrawerContent(nav, scaffoldState, sheetState, accountViewModel)
|
||||
|
@ -316,7 +316,7 @@ private fun CreateAndRenderPages(
|
||||
4 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav)
|
||||
5 -> TabBookmarks(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
|
||||
private fun DisplayLNAddress(
|
||||
fun DisplayLNAddress(
|
||||
lud16: String?,
|
||||
userHex: String,
|
||||
account: Account
|
||||
@ -1294,7 +1294,7 @@ private fun WatchReportsAndUpdateFeed(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TabRelays(user: User, accountViewModel: AccountViewModel) {
|
||||
fun TabRelays(user: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val feedViewModel: RelayFeedViewModel = viewModel()
|
||||
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
@ -1323,7 +1323,7 @@ fun TabRelays(user: User, accountViewModel: AccountViewModel) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
RelayFeedView(feedViewModel, accountViewModel, enablePullRefresh = false)
|
||||
RelayFeedView(feedViewModel, accountViewModel, enablePullRefresh = false, nav = nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -425,4 +425,27 @@
|
||||
<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="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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user