diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/RelaySetupInfo.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/RelaySetupInfo.kt index c9f099ed6..18782e29f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/RelaySetupInfo.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/RelaySetupInfo.kt @@ -14,4 +14,6 @@ data class RelaySetupInfo( val spamCount: Int = 0, val feedTypes: Set, val paidRelay: Boolean = false -) +) { + val briefInfo: RelayBriefInfo = RelayBriefInfo(url) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11Retriever.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11Retriever.kt new file mode 100644 index 000000000..58e88e207 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11Retriever.kt @@ -0,0 +1,103 @@ +package com.vitorpamplona.amethyst.service + +import android.util.Log +import android.util.LruCache +import com.vitorpamplona.amethyst.model.RelayInformation +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Request +import okhttp3.Response +import java.io.IOException + +object Nip11CachedRetriever { + val relayInformationDocumentCache = LruCache(100) + val retriever = Nip11Retriever() + + suspend fun loadRelayInfo( + dirtyUrl: String, + onInfo: (RelayInformation) -> Unit, + onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit + ) { + val url = retriever.cleanUrl(dirtyUrl) + val doc = relayInformationDocumentCache.get(url) + + if (doc != null) { + onInfo(doc) + } else { + Nip11Retriever().loadRelayInfo( + url, + dirtyUrl, + onInfo = { + relayInformationDocumentCache.put(url, it) + onInfo(it) + }, + onError + ) + } + } +} + +class Nip11Retriever { + enum class ErrorCode { + FAIL_TO_ASSEMBLE_URL, + FAIL_TO_REACH_SERVER, + FAIL_TO_PARSE_RESULT, + FAIL_WITH_HTTP_STATUS + } + + fun cleanUrl(dirtyUrl: String): String { + return if (dirtyUrl.contains("://")) { + dirtyUrl + .replace("wss://", "https://") + .replace("ws://", "http://") + } else { + "https://$dirtyUrl" + } + } + + suspend fun loadRelayInfo( + url: String, + dirtyUrl: String, + onInfo: (RelayInformation) -> Unit, + onError: (String, ErrorCode, String?) -> Unit + ) { + try { + val request: Request = Request + .Builder() + .header("Accept", "application/nostr+json") + .url(url) + .build() + + HttpClient.getHttpClient() + .newCall(request) + .enqueue( + object : Callback { + override fun onResponse(call: Call, response: Response) { + checkNotInMainThread() + response.use { + val body = it.body.string() + try { + if (it.isSuccessful) { + onInfo(RelayInformation.fromJson(body)) + } else { + onError(dirtyUrl, ErrorCode.FAIL_WITH_HTTP_STATUS, it.code.toString()) + } + } catch (e: Exception) { + Log.e("RelayInfoFail", "Resulting Message from Relay $dirtyUrl in not parseable: $body", e) + onError(dirtyUrl, ErrorCode.FAIL_TO_PARSE_RESULT, e.message) + } + } + } + + override fun onFailure(call: Call, e: IOException) { + Log.e("RelayInfoFail", "$dirtyUrl unavailable", e) + onError(dirtyUrl, ErrorCode.FAIL_TO_REACH_SERVER, e.message) + } + } + ) + } catch (e: Exception) { + Log.e("RelayInfoFail", "Invalid URL $dirtyUrl", e) + onError(dirtyUrl, ErrorCode.FAIL_TO_ASSEMBLE_URL, e.message) + } + } +} 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 3abfba9ef..1f58535f6 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 @@ -1,6 +1,5 @@ package com.vitorpamplona.amethyst.ui.actions -import android.content.Context import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable @@ -50,24 +49,35 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.RelayInformation +import com.vitorpamplona.amethyst.model.RelayBriefInfo import com.vitorpamplona.amethyst.model.RelaySetupInfo +import com.vitorpamplona.amethyst.service.Nip11Retriever +import com.vitorpamplona.amethyst.service.relays.Constants import com.vitorpamplona.amethyst.service.relays.Constants.defaultRelays import com.vitorpamplona.amethyst.service.relays.FeedType +import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.Font14SP +import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding +import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding +import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChat +import com.vitorpamplona.amethyst.ui.theme.Size30Modifier import com.vitorpamplona.amethyst.ui.theme.Size35dp +import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.amethyst.ui.theme.WarningColor +import com.vitorpamplona.amethyst.ui.theme.allGoodColor import com.vitorpamplona.amethyst.ui.theme.placeholderText -import kotlinx.coroutines.CoroutineScope +import com.vitorpamplona.amethyst.ui.theme.warningColor import kotlinx.coroutines.launch import java.lang.Math.round @@ -75,16 +85,9 @@ import java.lang.Math.round fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, relayToAdd: String = "", nav: (String) -> Unit) { val postViewModel: NewRelayListViewModel = viewModel() val feedState by postViewModel.relays.collectAsState() - val context = LocalContext.current - val scope = rememberCoroutineScope() LaunchedEffect(Unit) { postViewModel.load(accountViewModel.account) - postViewModel.relays.value.forEach { item -> - loadRelayInfo(item.url, context, scope) { - postViewModel.togglePaidRelay(item, it.limitation?.payment_required ?: false) - } - } } Dialog( @@ -96,7 +99,9 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re TopAppBar( title = { Row( - modifier = Modifier.fillMaxWidth().padding(end = 10.dp), + modifier = Modifier + .fillMaxWidth() + .padding(end = 10.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { @@ -108,11 +113,7 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re defaultRelays.forEach { postViewModel.addRelay(it) } - postViewModel.relays.value.forEach { item -> - loadRelayInfo(item.url, context, scope) { - postViewModel.togglePaidRelay(item, it.limitation?.payment_required ?: false) - } - } + postViewModel.loadRelayDocuments() } ) { Text(stringResource(R.string.default_relays)) @@ -139,8 +140,6 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re ) } ) { pad -> - val scope = rememberCoroutineScope() - Column( modifier = Modifier.padding( 16.dp, @@ -158,10 +157,6 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re ) ) { itemsIndexed(feedState, key = { _, item -> item.url }) { index, item -> - if (index == 0) { - ServerConfigHeader() - } - ServerConfig( item, onToggleDownload = { postViewModel.toggleDownload(it) }, @@ -175,9 +170,7 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re onDelete = { postViewModel.deleteRelay(it) }, accountViewModel = accountViewModel, - nav = nav, - scope = scope, - context = context + nav = nav ) } } @@ -261,13 +254,38 @@ fun ServerConfigHeader() { } } -@OptIn(ExperimentalFoundationApi::class) +@Preview +@Composable +fun ServerConfigPreview() { + ServerConfigClickableLine( + item = RelaySetupInfo( + url = "nostr.mom", + read = true, + write = true, + errorCount = 23, + downloadCountInBytes = 10000, + uploadCountInBytes = 10000000, + spamCount = 10, + feedTypes = Constants.activeTypesGlobalChats, + paidRelay = true + ), + onDelete = {}, + onToggleDownload = {}, + onToggleUpload = {}, + onToggleFollows = {}, + onTogglePrivateDMs = {}, + onTogglePublicChats = {}, + onToggleGlobal = {}, + onToggleSearch = {}, + onClick = {} + ) +} + @Composable fun ServerConfig( item: RelaySetupInfo, onToggleDownload: (RelaySetupInfo) -> Unit, onToggleUpload: (RelaySetupInfo) -> Unit, - onToggleFollows: (RelaySetupInfo) -> Unit, onTogglePrivateDMs: (RelaySetupInfo) -> Unit, onTogglePublicChats: (RelaySetupInfo) -> Unit, @@ -276,393 +294,112 @@ fun ServerConfig( onDelete: (RelaySetupInfo) -> Unit, accountViewModel: AccountViewModel, - nav: (String) -> Unit, - context: Context, - scope: CoroutineScope + nav: (String) -> Unit ) { - var relayInfo: RelayInformation? by remember { mutableStateOf(null) } + var relayInfo: RelayInfoDialog? by remember { mutableStateOf(null) } + val scope = rememberCoroutineScope() + val context = LocalContext.current - if (relayInfo != null) { + relayInfo?.let { RelayInformationDialog( - onClose = { - relayInfo = null - }, - relayInfo = relayInfo!!, - accountViewModel, - nav + onClose = { relayInfo = null }, + relayInfo = it.relayInfo, + relayBriefInfo = it.relayBriefInfo, + accountViewModel = accountViewModel, + nav = nav ) } + ServerConfigClickableLine( + item = item, + onToggleDownload = onToggleDownload, + onToggleUpload = onToggleUpload, + onToggleFollows = onToggleFollows, + onTogglePrivateDMs = onTogglePrivateDMs, + onTogglePublicChats = onTogglePublicChats, + onToggleGlobal = onToggleGlobal, + onToggleSearch = onToggleSearch, + onDelete = onDelete, + onClick = { + accountViewModel.retrieveRelayDocument( + item.url, + onInfo = { + relayInfo = RelayInfoDialog(RelayBriefInfo(item.url), it) + }, + onError = { url, errorCode, exceptionMessage -> + val msg = when (errorCode) { + Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + } + + scope.launch { + Toast + .makeText( + context, + msg, + Toast.LENGTH_SHORT + ) + .show() + } + } + ) + } + ) +} + +@Composable +fun ServerConfigClickableLine( + item: RelaySetupInfo, + onToggleDownload: (RelaySetupInfo) -> Unit, + onToggleUpload: (RelaySetupInfo) -> Unit, + onToggleFollows: (RelaySetupInfo) -> Unit, + onTogglePrivateDMs: (RelaySetupInfo) -> Unit, + onTogglePublicChats: (RelaySetupInfo) -> Unit, + onToggleGlobal: (RelaySetupInfo) -> Unit, + onToggleSearch: (RelaySetupInfo) -> Unit, + onDelete: (RelaySetupInfo) -> Unit, + onClick: () -> Unit +) { Column(Modifier.fillMaxWidth()) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp) ) { - Column() { - IconButton( - modifier = Modifier.size(30.dp), - onClick = { onDelete(item) } - ) { - Icon( - imageVector = Icons.Default.Cancel, - null, - modifier = Modifier - .padding(end = 5.dp) - .size(15.dp), - tint = Color.Red - ) - } + Column(Modifier.clickable(onClick = onClick)) { + RenderRelayIcon(iconUrl = item.briefInfo.favIcon, Size55dp) } - Column(Modifier.weight(1f)) { - Row(verticalAlignment = Alignment.CenterVertically) { - if (item.paidRelay) { - Icon( - imageVector = Icons.Default.Paid, - null, - modifier = Modifier - .padding(end = 5.dp) - .size(14.dp), - tint = Color.Green - ) - } + Spacer(modifier = HalfHorzPadding) - Text( - text = item.url.removePrefix("wss://").removeSuffix("/"), - modifier = Modifier - .weight(1f) - .clickable { - loadRelayInfo(item.url, context, scope) { - relayInfo = it - } - }, - maxLines = 1, - overflow = TextOverflow.Ellipsis + Column(Modifier.weight(1f)) { + FirstLine(item, onClick, onDelete, ReactionRowHeightChat.fillMaxWidth()) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = ReactionRowHeightChat.fillMaxWidth() + ) { + RenderActiveToggles( + item = item, + onToggleFollows = onToggleFollows, + onTogglePrivateDMs = onTogglePrivateDMs, + onTogglePublicChats = onTogglePublicChats, + onToggleGlobal = onToggleGlobal, + onToggleSearch = onToggleSearch ) } - Row(verticalAlignment = Alignment.CenterVertically) { - Column(Modifier.weight(1f)) { - Row(verticalAlignment = Alignment.CenterVertically) { - IconButton( - modifier = Modifier - .size(30.dp), - onClick = { } - ) { - Icon( - painterResource(R.drawable.ic_home), - stringResource(R.string.home_feed), - modifier = Modifier - .padding(end = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { onToggleFollows(item) }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.home_feed), - Toast.LENGTH_SHORT - ) - .show() - } - } - ), - tint = if (item.feedTypes.contains(FeedType.FOLLOWS)) { - Color.Green - } else { - MaterialTheme.colors.onSurface.copy( - alpha = 0.32f - ) - } - ) - } - IconButton( - modifier = Modifier.size(30.dp), - onClick = { } - ) { - Icon( - painterResource(R.drawable.ic_dm), - stringResource(R.string.private_message_feed), - modifier = Modifier - .padding(horizontal = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { onTogglePrivateDMs(item) }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.private_message_feed), - Toast.LENGTH_SHORT - ) - .show() - } - } - ), - tint = if (item.feedTypes.contains(FeedType.PRIVATE_DMS)) { - Color.Green - } else { - MaterialTheme.colors.onSurface.copy( - alpha = 0.32f - ) - } - ) - } - IconButton( - modifier = Modifier.size(30.dp), - onClick = { } - ) { - Icon( - imageVector = Icons.Default.Groups, - stringResource(R.string.public_chat_feed), - modifier = Modifier - .padding(horizontal = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { onTogglePublicChats(item) }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.public_chat_feed), - Toast.LENGTH_SHORT - ) - .show() - } - } - ), - tint = if (item.feedTypes.contains(FeedType.PUBLIC_CHATS)) { - Color.Green - } else { - MaterialTheme.colors.onSurface.copy( - alpha = 0.32f - ) - } - ) - } - IconButton( - modifier = Modifier.size(30.dp), - onClick = { } - ) { - Icon( - imageVector = Icons.Default.Public, - stringResource(R.string.global_feed), - modifier = Modifier - .padding(horizontal = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { onToggleGlobal(item) }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.global_feed), - Toast.LENGTH_SHORT - ) - .show() - } - } - ), - tint = if (item.feedTypes.contains(FeedType.GLOBAL)) { - Color.Green - } else { - MaterialTheme.colors.onSurface.copy( - alpha = 0.32f - ) - } - ) - } - - IconButton( - modifier = Modifier.size(30.dp), - onClick = { onToggleSearch(item) } - ) { - Icon( - imageVector = Icons.Default.Search, - stringResource(R.string.search_feed), - modifier = Modifier - .padding(horizontal = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { onToggleSearch(item) }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.search_feed), - Toast.LENGTH_SHORT - ) - .show() - } - } - ), - tint = if (item.feedTypes.contains(FeedType.SEARCH)) { - Color.Green - } else { - MaterialTheme.colors.onSurface.copy( - alpha = 0.32f - ) - } - ) - } - } - } - - Column(Modifier.weight(1.4f)) { - Row(verticalAlignment = Alignment.CenterVertically) { - IconButton( - modifier = Modifier.size(30.dp), - onClick = { } - ) { - Icon( - imageVector = Icons.Default.Download, - stringResource(R.string.read_from_relay), - modifier = Modifier - .padding(horizontal = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { onToggleDownload(item) }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.read_from_relay), - Toast.LENGTH_SHORT - ) - .show() - } - } - ), - tint = if (item.read) { - Color.Green - } else { - MaterialTheme.colors.onSurface.copy( - alpha = 0.32f - ) - } - ) - } - - Text( - text = "${countToHumanReadable(item.downloadCountInBytes)}", - maxLines = 1, - fontSize = 12.sp, - modifier = Modifier.weight(1.2f), - color = MaterialTheme.colors.placeholderText - ) - - IconButton( - modifier = Modifier.size(30.dp), - onClick = { } - ) { - Icon( - imageVector = Icons.Default.Upload, - stringResource(R.string.write_to_relay), - modifier = Modifier - .padding(horizontal = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { onToggleUpload(item) }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.write_to_relay), - Toast.LENGTH_SHORT - ) - .show() - } - } - ), - tint = if (item.write) { - Color.Green - } else { - MaterialTheme.colors.onSurface.copy( - alpha = 0.32f - ) - } - ) - } - - Text( - text = "${countToHumanReadable(item.uploadCountInBytes)}", - maxLines = 1, - fontSize = 12.sp, - modifier = Modifier.weight(1.2f), - color = MaterialTheme.colors.placeholderText - ) - - Icon( - imageVector = Icons.Default.SyncProblem, - stringResource(R.string.errors), - modifier = Modifier - .padding(horizontal = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.errors), - Toast.LENGTH_SHORT - ) - .show() - } - } - ), - tint = if (item.errorCount > 0) Color.Yellow else Color.Green - ) - - Text( - text = "${countToHumanReadable(item.errorCount)}", - maxLines = 1, - fontSize = 12.sp, - modifier = Modifier.weight(1f), - color = MaterialTheme.colors.placeholderText - ) - - Icon( - imageVector = Icons.Default.DeleteSweep, - stringResource(R.string.spam), - modifier = Modifier - .padding(horizontal = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.spam), - Toast.LENGTH_SHORT - ) - .show() - } - } - ), - tint = if (item.spamCount > 0) Color.Yellow else Color.Green - ) - - Text( - text = "${countToHumanReadable(item.spamCount)}", - maxLines = 1, - fontSize = 12.sp, - modifier = Modifier.weight(1f), - color = MaterialTheme.colors.placeholderText - ) - } - } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = ReactionRowHeightChat.fillMaxWidth() + ) { + RenderStatusRow( + item = item, + onToggleDownload = onToggleDownload, + onToggleUpload = onToggleUpload, + modifier = HalfStartPadding.weight(1f) + ) } } } @@ -673,6 +410,384 @@ fun ServerConfig( } } +@Composable +@OptIn(ExperimentalFoundationApi::class) +private fun RenderStatusRow( + item: RelaySetupInfo, + onToggleDownload: (RelaySetupInfo) -> Unit, + onToggleUpload: (RelaySetupInfo) -> Unit, + modifier: Modifier +) { + val scope = rememberCoroutineScope() + val context = LocalContext.current + + Icon( + imageVector = Icons.Default.Download, + contentDescription = stringResource(R.string.read_from_relay), + modifier = Modifier + .size(15.dp) + .combinedClickable( + onClick = { onToggleDownload(item) }, + onLongClick = { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.read_from_relay), + Toast.LENGTH_SHORT + ) + .show() + } + } + ), + tint = if (item.read) { + MaterialTheme.colors.allGoodColor + } else { + MaterialTheme.colors.onSurface.copy( + alpha = 0.32f + ) + } + ) + + Text( + text = countToHumanReadableBytes(item.downloadCountInBytes), + maxLines = 1, + fontSize = 12.sp, + modifier = modifier, + color = MaterialTheme.colors.placeholderText + ) + + Icon( + imageVector = Icons.Default.Upload, + stringResource(R.string.write_to_relay), + modifier = Modifier + .size(15.dp) + .combinedClickable( + onClick = { onToggleUpload(item) }, + onLongClick = { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.write_to_relay), + Toast.LENGTH_SHORT + ) + .show() + } + } + ), + tint = if (item.write) { + MaterialTheme.colors.allGoodColor + } else { + MaterialTheme.colors.onSurface.copy( + alpha = 0.32f + ) + } + ) + + Text( + text = countToHumanReadableBytes(item.uploadCountInBytes), + maxLines = 1, + fontSize = 12.sp, + modifier = modifier, + color = MaterialTheme.colors.placeholderText + ) + + Icon( + imageVector = Icons.Default.SyncProblem, + stringResource(R.string.errors), + modifier = Modifier + .size(15.dp) + .combinedClickable( + onClick = { }, + onLongClick = { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.errors), + Toast.LENGTH_SHORT + ) + .show() + } + } + ), + tint = if (item.errorCount > 0) MaterialTheme.colors.warningColor else MaterialTheme.colors.allGoodColor + ) + + Text( + text = countToHumanReadable(item.errorCount, "errors"), + maxLines = 1, + fontSize = 12.sp, + modifier = modifier, + color = MaterialTheme.colors.placeholderText + ) + + Icon( + imageVector = Icons.Default.DeleteSweep, + stringResource(R.string.spam), + modifier = Modifier + .size(15.dp) + .combinedClickable( + onClick = { }, + onLongClick = { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.spam), + Toast.LENGTH_SHORT + ) + .show() + } + } + ), + tint = if (item.spamCount > 0) MaterialTheme.colors.warningColor else MaterialTheme.colors.allGoodColor + ) + + Text( + text = countToHumanReadable(item.spamCount, "spam"), + maxLines = 1, + fontSize = 12.sp, + modifier = modifier, + color = MaterialTheme.colors.placeholderText + ) +} + +@Composable +@OptIn(ExperimentalFoundationApi::class) +private fun RenderActiveToggles( + item: RelaySetupInfo, + onToggleFollows: (RelaySetupInfo) -> Unit, + onTogglePrivateDMs: (RelaySetupInfo) -> Unit, + onTogglePublicChats: (RelaySetupInfo) -> Unit, + onToggleGlobal: (RelaySetupInfo) -> Unit, + onToggleSearch: (RelaySetupInfo) -> Unit +) { + val scope = rememberCoroutineScope() + val context = LocalContext.current + + Text( + text = stringResource(id = R.string.active_for), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colors.placeholderText, + modifier = Modifier.padding(start = 2.dp, end = 5.dp), + fontSize = 14.sp + ) + + IconButton( + modifier = Size30Modifier, + onClick = { onToggleFollows(item) } + ) { + Icon( + painterResource(R.drawable.ic_home), + stringResource(R.string.home_feed), + modifier = Modifier + .padding(horizontal = 5.dp) + .size(15.dp) + .combinedClickable( + onClick = { onToggleFollows(item) }, + onLongClick = { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.home_feed), + Toast.LENGTH_SHORT + ) + .show() + } + } + ), + tint = if (item.feedTypes.contains(FeedType.FOLLOWS)) { + MaterialTheme.colors.allGoodColor + } else { + MaterialTheme.colors.onSurface.copy( + alpha = 0.32f + ) + } + ) + } + IconButton( + modifier = Size30Modifier, + onClick = { onTogglePrivateDMs(item) } + ) { + Icon( + painterResource(R.drawable.ic_dm), + stringResource(R.string.private_message_feed), + modifier = Modifier + .padding(horizontal = 5.dp) + .size(15.dp) + .combinedClickable( + onClick = { onTogglePrivateDMs(item) }, + onLongClick = { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.private_message_feed), + Toast.LENGTH_SHORT + ) + .show() + } + } + ), + tint = if (item.feedTypes.contains(FeedType.PRIVATE_DMS)) { + MaterialTheme.colors.allGoodColor + } else { + MaterialTheme.colors.onSurface.copy( + alpha = 0.32f + ) + } + ) + } + IconButton( + modifier = Size30Modifier, + onClick = { onTogglePublicChats(item) } + ) { + Icon( + imageVector = Icons.Default.Groups, + contentDescription = stringResource(R.string.public_chat_feed), + modifier = Modifier + .padding(horizontal = 5.dp) + .size(15.dp) + .combinedClickable( + onClick = { onTogglePublicChats(item) }, + onLongClick = { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.public_chat_feed), + Toast.LENGTH_SHORT + ) + .show() + } + } + ), + tint = if (item.feedTypes.contains(FeedType.PUBLIC_CHATS)) { + MaterialTheme.colors.allGoodColor + } else { + MaterialTheme.colors.onSurface.copy( + alpha = 0.32f + ) + } + ) + } + IconButton( + modifier = Size30Modifier, + onClick = { onToggleGlobal(item) } + ) { + Icon( + imageVector = Icons.Default.Public, + stringResource(R.string.global_feed), + modifier = Modifier + .padding(horizontal = 5.dp) + .size(15.dp) + .combinedClickable( + onClick = { onToggleGlobal(item) }, + onLongClick = { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.global_feed), + Toast.LENGTH_SHORT + ) + .show() + } + } + ), + tint = if (item.feedTypes.contains(FeedType.GLOBAL)) { + MaterialTheme.colors.allGoodColor + } else { + MaterialTheme.colors.onSurface.copy( + alpha = 0.32f + ) + } + ) + } + + IconButton( + modifier = Size30Modifier, + onClick = { onToggleSearch(item) } + ) { + Icon( + imageVector = Icons.Default.Search, + stringResource(R.string.search_feed), + modifier = Modifier + .padding(horizontal = 5.dp) + .size(15.dp) + .combinedClickable( + onClick = { onToggleSearch(item) }, + onLongClick = { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.search_feed), + Toast.LENGTH_SHORT + ) + .show() + } + } + ), + tint = if (item.feedTypes.contains(FeedType.SEARCH)) { + MaterialTheme.colors.allGoodColor + } else { + MaterialTheme.colors.onSurface.copy( + alpha = 0.32f + ) + } + ) + } +} + +@Composable +private fun FirstLine( + item: RelaySetupInfo, + onClick: () -> Unit, + onDelete: (RelaySetupInfo) -> Unit, + modifier: Modifier +) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) { + Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) { + Text( + text = item.briefInfo.displayUrl, + modifier = Modifier.clickable(onClick = onClick), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + if (item.paidRelay) { + Icon( + imageVector = Icons.Default.Paid, + null, + modifier = Modifier + .padding(start = 5.dp, top = 1.dp) + .size(14.dp), + tint = MaterialTheme.colors.allGoodColor + ) + } + } + + IconButton( + modifier = Modifier.size(30.dp), + onClick = { onDelete(item) } + ) { + Icon( + imageVector = Icons.Default.Cancel, + null, + modifier = Modifier + .padding(start = 10.dp) + .size(15.dp), + tint = WarningColor + ) + } + } +} + @Composable fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Unit) { var url by remember { mutableStateOf(relayToAdd) } @@ -702,7 +817,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni modifier = Modifier .size(Size35dp) .padding(horizontal = 5.dp), - tint = if (read) Color.Green else MaterialTheme.colors.placeholderText + tint = if (read) MaterialTheme.colors.allGoodColor else MaterialTheme.colors.placeholderText ) } @@ -713,7 +828,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni modifier = Modifier .size(Size35dp) .padding(horizontal = 5.dp), - tint = if (write) Color.Green else MaterialTheme.colors.placeholderText + tint = if (write) MaterialTheme.colors.allGoodColor else MaterialTheme.colors.placeholderText ) } @@ -739,9 +854,16 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni } } -fun countToHumanReadable(counter: Int) = when { - counter >= 1000000000 -> "${round(counter / 1000000000f)}G" - counter >= 1000000 -> "${round(counter / 1000000f)}M" - counter >= 1000 -> "${round(counter / 1000f)}k" +fun countToHumanReadableBytes(counter: Int) = when { + counter >= 1000000000 -> "${round(counter / 1000000000f)} GB" + counter >= 1000000 -> "${round(counter / 1000000f)} MB" + counter >= 1000 -> "${round(counter / 1000f)} KB" else -> "$counter" } + +fun countToHumanReadable(counter: Int, str: String) = when { + counter >= 1000000000 -> "${round(counter / 1000000000f)}G $str" + counter >= 1000000 -> "${round(counter / 1000000f)}M $str" + counter >= 1000 -> "${round(counter / 1000f)}K $str" + else -> "$counter $str" +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt index f6f03f0fe..885b28b07 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.RelaySetupInfo +import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.relays.Constants import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.RelayPool @@ -24,6 +25,7 @@ class NewRelayListViewModel : ViewModel() { fun load(account: Account) { this.account = account clear() + loadRelayDocuments() } fun create() { @@ -36,6 +38,21 @@ class NewRelayListViewModel : ViewModel() { clear() } + fun loadRelayDocuments() { + viewModelScope.launch(Dispatchers.IO) { + _relays.value.forEach { item -> + Nip11CachedRetriever.loadRelayInfo( + dirtyUrl = item.url, + onInfo = { + togglePaidRelay(item, it.limitation?.payment_required ?: false) + }, + onError = { url, errorCode, exceptionMessage -> + } + ) + } + } + } + fun clear() { _relays.update { var relayFile = account.userProfile().latestContactList?.relays() 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 index 952f455e0..9fbaf2463 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt @@ -1,8 +1,5 @@ package com.vitorpamplona.amethyst.ui.actions -import android.content.Context -import android.util.Log -import android.widget.Toast import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -28,27 +25,24 @@ 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.RelayBriefInfo import com.vitorpamplona.amethyst.model.RelayInformation -import com.vitorpamplona.amethyst.service.HttpClient -import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.components.ClickableEmail import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.note.LoadUser +import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import okhttp3.Call -import okhttp3.Callback -import okhttp3.Request -import okhttp3.Response -import java.io.IOException +import com.vitorpamplona.amethyst.ui.theme.Size55dp +import com.vitorpamplona.amethyst.ui.theme.StdPadding @OptIn(ExperimentalLayoutApi::class) @Composable fun RelayInformationDialog( onClose: () -> Unit, + relayBriefInfo: RelayBriefInfo, relayInfo: RelayInformation, accountViewModel: AccountViewModel, nav: (String) -> Unit @@ -79,18 +73,25 @@ fun RelayInformationDialog( }) } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - Title(relayInfo.name ?: "") - } + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, modifier = StdPadding.fillMaxWidth()) { + Column() { + RenderRelayIcon( + relayBriefInfo.favIcon, + Size55dp + ) + } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - SectionContent(relayInfo.description ?: "") + Spacer(modifier = DoubleHorzSpacer) + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Row() { + Title(relayInfo.name?.trim() ?: "") + } + + Row() { + SubtitleContent(relayInfo.description?.trim() ?: "") + } + } } Section(stringResource(R.string.owner)) @@ -212,7 +213,7 @@ private fun DisplaySupportedNips(relayInfo: RelayInformation) { val text = item.toString().padStart(2, '0') Box(Modifier.padding(10.dp)) { ClickableUrl( - urlText = "$text", + urlText = text, url = "https://github.com/nostr-protocol/nips/blob/master/$text.md" ) } @@ -222,7 +223,7 @@ private fun DisplaySupportedNips(relayInfo: RelayInformation) { val text = item.padStart(2, '0') Box(Modifier.padding(10.dp)) { ClickableUrl( - urlText = "$text", + urlText = text, url = "https://github.com/nostr-protocol/nips/blob/master/$text.md" ) } @@ -258,13 +259,18 @@ private fun DisplayOwnerInformation( @Composable fun Title(text: String) { - Spacer(modifier = DoubleVertSpacer) Text( text = text, fontWeight = FontWeight.Bold, fontSize = 24.sp ) - Spacer(modifier = DoubleVertSpacer) +} + +@Composable +fun SubtitleContent(text: String) { + Text( + text = text + ) } @Composable @@ -285,85 +291,3 @@ fun SectionContent(text: String) { text = text ) } - -fun loadRelayInfo( - dirtyUrl: String, - context: Context, - scope: CoroutineScope, - onInfo: (RelayInformation) -> Unit -) { - try { - val url = if (dirtyUrl.contains("://")) { - dirtyUrl - .replace("wss://", "https://") - .replace("ws://", "http://") - } else { - "https://$dirtyUrl" - } - - val request: Request = Request - .Builder() - .header("Accept", "application/nostr+json") - .url(url) - .build() - - HttpClient.getHttpClient() - .newCall(request) - .enqueue( - object : Callback { - override fun onResponse(call: Call, response: Response) { - checkNotInMainThread() - response.use { - val body = it.body.string() - try { - if (it.isSuccessful) { - onInfo(RelayInformation.fromJson(body)) - } else { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.an_error_occurred_trying_to_get_relay_information, dirtyUrl), - Toast.LENGTH_SHORT - ).show() - } - } - } catch (e: Exception) { - Log.e("RelayInfoFail", "Resulting Message from Relay $dirtyUrl in not parseable: $body", e) - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.an_error_occurred_trying_to_get_relay_information, dirtyUrl), - Toast.LENGTH_SHORT - ).show() - } - } - } - } - - override fun onFailure(call: Call, e: IOException) { - Log.e("RelayInfoFail", "$dirtyUrl unavailable", e) - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.an_error_occurred_trying_to_get_relay_information, dirtyUrl), - Toast.LENGTH_SHORT - ).show() - } - } - } - ) - } catch (e: Exception) { - Log.e("RelayInfoFail", "Invalid URL $dirtyUrl", e) - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.an_error_occurred_trying_to_get_relay_information, dirtyUrl), - Toast.LENGTH_SHORT - ).show() - } - } -} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt index b979dc2fe..31028d215 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.RelayBriefInfo import com.vitorpamplona.amethyst.model.RelayInformation +import com.vitorpamplona.amethyst.service.Nip11Retriever import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.launch @@ -41,6 +42,11 @@ data class RelayList( val isSelected: Boolean ) +data class RelayInfoDialog( + val relayBriefInfo: RelayBriefInfo, + val relayInfo: RelayInformation +) + @Composable fun RelaySelectionDialog( preSelectedList: List, @@ -63,16 +69,17 @@ fun RelaySelectionDialog( } ) } - var relayInfo: RelayInformation? by remember { mutableStateOf(null) } + var relayInfo: RelayInfoDialog? by remember { mutableStateOf(null) } - if (relayInfo != null) { + relayInfo?.let { RelayInformationDialog( onClose = { relayInfo = null }, - relayInfo = relayInfo!!, - accountViewModel, - nav + relayInfo = it.relayInfo, + relayBriefInfo = it.relayBriefInfo, + accountViewModel = accountViewModel, + nav = nav ) } @@ -161,9 +168,30 @@ fun RelaySelectionDialog( } }, onLongPress = { - loadRelayInfo(item.relay.url, context, scope) { - relayInfo = it - } + accountViewModel.retrieveRelayDocument( + item.relay.url, + onInfo = { + relayInfo = RelayInfoDialog(RelayBriefInfo(item.relay.url), it) + }, + onError = { url, errorCode, exceptionMessage -> + val msg = when (errorCode) { + Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + } + + scope.launch { + Toast + .makeText( + context, + msg, + Toast.LENGTH_SHORT + ) + .show() + } + } + ) } ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt index 8c3736791..c0a97c319 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.ui.note +import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -28,14 +29,15 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.map import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.RelayBriefInfo import com.vitorpamplona.amethyst.model.RelayInformation +import com.vitorpamplona.amethyst.service.Nip11Retriever import com.vitorpamplona.amethyst.ui.actions.RelayInformationDialog -import com.vitorpamplona.amethyst.ui.actions.loadRelayInfo import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter @@ -45,6 +47,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size15dp import com.vitorpamplona.amethyst.ui.theme.StdStartPadding import com.vitorpamplona.amethyst.ui.theme.placeholderText import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.launch @Composable public fun RelayBadgesHorizontal(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) { @@ -112,8 +115,9 @@ fun RenderRelay(relay: RelayBriefInfo, accountViewModel: AccountViewModel, nav: relayInfo = null }, relayInfo = relayInfo!!, - accountViewModel, - nav + relayBriefInfo = relay, + accountViewModel = accountViewModel, + nav = nav ) } @@ -131,9 +135,30 @@ fun RenderRelay(relay: RelayBriefInfo, accountViewModel: AccountViewModel, nav: interactionSource = interactionSource, indication = ripple, onClick = { - loadRelayInfo(relay.url, context, scope) { - relayInfo = it - } + accountViewModel.retrieveRelayDocument( + relay.url, + onInfo = { + relayInfo = it + }, + onError = { url, errorCode, exceptionMessage -> + val msg = when (errorCode) { + Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + } + + scope.launch { + Toast + .makeText( + context, + msg, + Toast.LENGTH_SHORT + ) + .show() + } + } + ) } ) } @@ -146,12 +171,12 @@ fun RenderRelay(relay: RelayBriefInfo, accountViewModel: AccountViewModel, nav: } @Composable -private fun RenderRelayIcon(iconUrl: String) { +fun RenderRelayIcon(iconUrl: String, size: Dp = Size13dp) { val backgroundColor = MaterialTheme.colors.background val iconModifier = remember { Modifier - .size(Size13dp) + .size(size) .clip(shape = CircleShape) .background(backgroundColor) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 76bb6ab2d..b105b5216 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -19,10 +19,13 @@ import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.RelayInformation import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.service.Nip05Verifier +import com.vitorpamplona.amethyst.service.Nip11CachedRetriever +import com.vitorpamplona.amethyst.service.Nip11Retriever import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.ui.components.UrlPreviewState @@ -551,6 +554,16 @@ class AccountViewModel(val account: Account) : ViewModel() { } } + fun retrieveRelayDocument( + dirtyUrl: String, + onInfo: (RelayInformation) -> Unit, + onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit + ) { + viewModelScope.launch(Dispatchers.IO) { + Nip11CachedRetriever.loadRelayInfo(dirtyUrl, onInfo, onError) + } + } + class Factory(val account: Account) : ViewModelProvider.Factory { override fun create(modelClass: Class): AccountViewModel { return AccountViewModel(account) as AccountViewModel diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt index e4b1d22bb..538442536 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt @@ -24,3 +24,9 @@ val DarkerGreen = Color.Green.copy(alpha = 0.32f) val WarningColor = Color(0xFFC62828) val RelayIconFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0.5f) }) + +val LightWarningColor = Color(0xFFffcc00) +val DarkWarningColor = Color(0xFFF8DE22) + +val LightAllGoodColor = Color(0xFF339900) +val DarkAllGoodColor = Color(0xFF99cc33) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 42a6331c6..541420948 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -286,6 +286,12 @@ val Colors.overPictureBackground: Color val Colors.bitcoinColor: Color get() = if (isLight) BitcoinLight else BitcoinDark +val Colors.warningColor: Color + get() = if (isLight) LightWarningColor else DarkWarningColor + +val Colors.allGoodColor: Color + get() = if (isLight) LightAllGoodColor else DarkAllGoodColor + val Colors.markdownStyle: RichTextStyle get() = if (isLight) MarkDownStyleOnLight else MarkDownStyleOnDark diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e127e7523..33f12e712 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -558,4 +558,16 @@ Unable to send zap Message the User Ok + + Failed to reach %1$s: %2$s + Failed to reach %1$s: %2$s + Failed to parse result from %1$s: %2$s + %1$s failed with code %2$s + Active for: + + Home + DMs + Chats + Global + Search