diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/RelayInformation.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/RelayInformation.kt new file mode 100644 index 000000000..d2496ce42 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/RelayInformation.kt @@ -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?, + val supported_nip_extensions: List?, + val software: String?, + val version: String?, + val limitation: RelayInformationLimitation?, + val relay_countries: List?, + val language_tags: List?, + val tags: List?, + 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? +) { + 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 { + 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?, + val subscription: List?, + val publication: List? +) { + 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 { + 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 { + 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 { + 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) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt index 22fdf2390..a26776310 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt @@ -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() } } ), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt new file mode 100644 index 000000000..da8277af9 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt @@ -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 + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index c8619c40c..11ae46524 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -95,7 +95,8 @@ fun AppTopBar( followLists: FollowListViewModel, navEntryState: State, 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)) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 4342e5486..c99f11af3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -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) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt index 8e7ab4681..498ebc65c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt @@ -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) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index 9fb6e3387..ed52746d3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -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) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 0279de643..8eda11841 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -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) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3e1c422f3..51f7e4f2e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -425,4 +425,27 @@ Zapraiser at %1$s. %2$s sats to goal Read from Relay Write to Relay + An error ocurred trying to get relay information + Owner + Version + Software + Contact + Supports + Admission Fees + Payments url + Limitations + Countries + Languages + Tags + Posting policy + Message length + Subscriptions + Filters + Subscription id length + Minimum prefix + Maximum event tags + Content length + Minimum PoW + Auth + Payment