diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 37904c7a2..291209b76 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji import com.vitorpamplona.amethyst.service.relays.EOSETime import com.vitorpamplona.amethyst.service.relays.Relay -import com.vitorpamplona.amethyst.ui.actions.updated +import com.vitorpamplona.amethyst.ui.actions.relays.updated import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.note.combineWith import com.vitorpamplona.amethyst.ui.note.toShortenHex diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AddDMRelayListDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AddDMRelayListDialog.kt new file mode 100644 index 000000000..90f684c6d --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AddDMRelayListDialog.kt @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.actions.relays + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +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.ui.actions.CloseButton +import com.vitorpamplona.amethyst.ui.actions.SaveButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.amethyst.ui.theme.imageModifier + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddDMRelayListDialog( + onClose: () -> Unit, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val postViewModel: DMRelayListViewModel = viewModel() + + LaunchedEffect(Unit) { postViewModel.load(accountViewModel.account) } + + Dialog( + onDismissRequest = onClose, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + Scaffold( + topBar = { + TopAppBar( + title = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = StdHorzSpacer) + + Text(stringResource(R.string.dm_relays_title)) + + SaveButton( + onPost = { + postViewModel.create() + onClose() + }, + true, + ) + } + }, + navigationIcon = { + Spacer(modifier = StdHorzSpacer) + CloseButton( + onPress = { + postViewModel.clear() + onClose() + }, + ) + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + ) + }, + ) { pad -> + Column( + modifier = + Modifier.padding( + 16.dp, + pad.calculateTopPadding(), + 16.dp, + pad.calculateBottomPadding(), + ), + verticalArrangement = Arrangement.SpaceAround, + ) { + Explanation() + + DMRelayList(postViewModel, accountViewModel, onClose, nav) + } + } + } +} + +@Composable +private fun Explanation() { + Card(modifier = MaterialTheme.colorScheme.imageModifier) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = stringResource(id = R.string.dm_relays_not_found_editing), + ) + + Spacer(modifier = StdVertSpacer) + + Text( + text = stringResource(id = R.string.dm_relays_not_found_examples), + ) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AllRelayListView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AllRelayListView.kt new file mode 100644 index 000000000..e3007479f --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AllRelayListView.kt @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.actions.relays + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.service.relays.Constants.defaultRelays +import com.vitorpamplona.amethyst.ui.actions.CloseButton +import com.vitorpamplona.amethyst.ui.actions.SaveButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AllRelayListView( + onClose: () -> Unit, + relayToAdd: String = "", + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val postViewModel: Kind3RelayListViewModel = viewModel() + val dmViewModel: DMRelayListViewModel = viewModel() + val feedState by postViewModel.relays.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { postViewModel.load(accountViewModel.account) } + + Dialog( + onDismissRequest = onClose, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + Scaffold( + topBar = { + TopAppBar( + title = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = StdHorzSpacer) + + Button( + onClick = { + postViewModel.deleteAll() + defaultRelays.forEach { postViewModel.addRelay(it) } + postViewModel.loadRelayDocuments() + }, + ) { + Text(stringResource(R.string.default_relays)) + } + + SaveButton( + onPost = { + postViewModel.create() + onClose() + }, + true, + ) + } + }, + navigationIcon = { + Spacer(modifier = DoubleHorzSpacer) + CloseButton( + onPress = { + postViewModel.clear() + onClose() + }, + ) + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + ) + }, + ) { pad -> + Column( + modifier = + Modifier.padding( + 16.dp, + pad.calculateTopPadding(), + 16.dp, + pad.calculateBottomPadding(), + ), + verticalArrangement = Arrangement.SpaceAround, + ) { + Kind3RelayListView(feedState, postViewModel, accountViewModel, onClose, nav, relayToAdd) + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/ByteFormatter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/ByteFormatter.kt new file mode 100644 index 000000000..7a5bced1f --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/ByteFormatter.kt @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.actions.relays + +fun countToHumanReadableBytes(counter: Int) = + when { + counter >= 1000000000 -> "${Math.round(counter / 1000000000f)} GB" + counter >= 1000000 -> "${Math.round(counter / 1000000f)} MB" + counter >= 1000 -> "${Math.round(counter / 1000f)} KB" + else -> "$counter" + } + +fun countToHumanReadable( + counter: Int, + str: String, +) = when { + counter >= 1000000000 -> "${Math.round(counter / 1000000000f)}G $str" + counter >= 1000000 -> "${Math.round(counter / 1000000f)}M $str" + counter >= 1000 -> "${Math.round(counter / 1000f)}K $str" + else -> "$counter $str" +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/DMRelayListView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/DMRelayListView.kt similarity index 76% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/actions/DMRelayListView.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/DMRelayListView.kt index efe3b53f9..20f833691 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/DMRelayListView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/DMRelayListView.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.actions +package com.vitorpamplona.amethyst.ui.actions.relays import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi @@ -42,19 +42,13 @@ import androidx.compose.material.icons.filled.SyncProblem import androidx.compose.material.icons.filled.Upload import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -68,14 +62,13 @@ import androidx.compose.ui.res.stringResource 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 androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.RelayBriefInfoCache import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.Nip11Retriever +import com.vitorpamplona.amethyst.ui.actions.RelayInfoDialog +import com.vitorpamplona.amethyst.ui.actions.RelayInformationDialog import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.ButtonBorder @@ -85,118 +78,44 @@ 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.Size10dp -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.imageModifier import com.vitorpamplona.amethyst.ui.theme.largeRelayIconModifier import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.warningColor +import com.vitorpamplona.quartz.encoders.RelayUrlFormatter import kotlinx.coroutines.launch -@OptIn(ExperimentalMaterial3Api::class) @Composable -fun DMRelayListView( - onClose: () -> Unit, +fun DMRelayList( + postViewModel: DMRelayListViewModel, accountViewModel: AccountViewModel, - relayToAdd: String = "", + onClose: () -> Unit, nav: (String) -> Unit, ) { - val postViewModel: DMRelayListViewModel = viewModel() val feedState by postViewModel.relays.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { postViewModel.load(accountViewModel.account) } - - Dialog( - onDismissRequest = onClose, - properties = DialogProperties(usePlatformDefaultWidth = false), - ) { - Scaffold( - topBar = { - TopAppBar( - title = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(modifier = StdHorzSpacer) - - Text(stringResource(R.string.dm_relays_title)) - - SaveButton( - onPost = { - postViewModel.create() - onClose() - }, - true, - ) - } - }, - navigationIcon = { - Spacer(modifier = StdHorzSpacer) - CloseButton( - onPress = { - postViewModel.clear() - onClose() - }, - ) - }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), - ) - }, - ) { pad -> - Column( - modifier = - Modifier.padding( - 16.dp, - pad.calculateTopPadding(), - 16.dp, - pad.calculateBottomPadding(), - ), - verticalArrangement = Arrangement.SpaceAround, - ) { - Card(modifier = MaterialTheme.colorScheme.imageModifier) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = stringResource(id = R.string.dm_relays_not_found_editing), - ) - - Spacer(modifier = StdVertSpacer) - - Text( - text = stringResource(id = R.string.dm_relays_not_found_examples), - ) - } + Row(verticalAlignment = Alignment.CenterVertically) { + LazyColumn( + contentPadding = FeedPadding, + ) { + itemsIndexed(feedState, key = { _, item -> item.url }) { index, item -> + DMServerConfig( + item, + onDelete = { postViewModel.deleteRelay(item) }, + accountViewModel = accountViewModel, + ) { + onClose() + nav(it) } - - Row(verticalAlignment = Alignment.CenterVertically) { - LazyColumn( - contentPadding = FeedPadding, - ) { - itemsIndexed(feedState, key = { _, item -> item.url }) { index, item -> - DMServerConfig( - item, - onDelete = { postViewModel.deleteRelay(item) }, - accountViewModel = accountViewModel, - ) { - onClose() - nav(it) - } - } - } - } - - Spacer(modifier = StdVertSpacer) - - DMEditableServerConfig(relayToAdd) { postViewModel.addRelay(it) } } } } + + Spacer(modifier = StdVertSpacer) + + DMEditableServerConfig { postViewModel.addRelay(it) } } @Composable @@ -308,7 +227,7 @@ fun DMServerConfigClickableLine( verticalAlignment = Alignment.CenterVertically, modifier = ReactionRowHeightChat.fillMaxWidth(), ) { - RenderStatusRow( + StatusRow( item = item, modifier = HalfStartPadding.weight(1f), accountViewModel = accountViewModel, @@ -323,7 +242,7 @@ fun DMServerConfigClickableLine( @Composable @OptIn(ExperimentalFoundationApi::class) -private fun RenderStatusRow( +private fun StatusRow( item: DMRelayListViewModel.DMRelaySetupInfo, modifier: Modifier, accountViewModel: AccountViewModel, @@ -502,11 +421,8 @@ private fun FirstLine( } @Composable -fun DMEditableServerConfig( - relayToAdd: String, - onNewRelay: (DMRelayListViewModel.DMRelaySetupInfo) -> Unit, -) { - var url by remember { mutableStateOf(relayToAdd) } +fun DMEditableServerConfig(onNewRelay: (DMRelayListViewModel.DMRelaySetupInfo) -> Unit) { + var url by remember { mutableStateOf("") } Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(Size10dp)) { OutlinedTextField( @@ -527,17 +443,7 @@ fun DMEditableServerConfig( Button( onClick = { if (url.isNotBlank() && url != "/") { - var addedWSS = - if (!url.startsWith("wss://") && !url.startsWith("ws://")) { - if (url.endsWith(".onion") || url.endsWith(".onion/")) { - "ws://$url" - } else { - "wss://$url" - } - } else { - url - } - if (url.endsWith("/")) addedWSS = addedWSS.dropLast(1) + val addedWSS = RelayUrlFormatter.normalize(url) onNewRelay(DMRelayListViewModel.DMRelaySetupInfo(addedWSS)) url = "" } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/DMRelayListViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/DMRelayListViewModel.kt similarity index 98% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/actions/DMRelayListViewModel.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/DMRelayListViewModel.kt index c65e92065..090d1ffda 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/DMRelayListViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/DMRelayListViewModel.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.actions +package com.vitorpamplona.amethyst.ui.actions.relays import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListView.kt similarity index 76% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListView.kt index 2be259d7b..1963bd87a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListView.kt @@ -18,13 +18,12 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.actions +package com.vitorpamplona.amethyst.ui.actions.relays 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 import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -45,18 +44,13 @@ import androidx.compose.material.icons.filled.SyncProblem import androidx.compose.material.icons.filled.Upload import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -72,31 +66,25 @@ 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.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.RelayBriefInfoCache import com.vitorpamplona.amethyst.model.RelaySetupInfo import com.vitorpamplona.amethyst.service.Nip11CachedRetriever 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.actions.RelayInfoDialog +import com.vitorpamplona.amethyst.ui.actions.RelayInformationDialog 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.DividerThickness -import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.FeedPadding -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.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.WarningColor import com.vitorpamplona.amethyst.ui.theme.allGoodColor @@ -105,183 +93,49 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.warningColor import com.vitorpamplona.quartz.encoders.RelayUrlFormatter import kotlinx.coroutines.launch -import java.lang.Math.round -@OptIn(ExperimentalMaterial3Api::class) @Composable -fun NewRelayListView( - onClose: () -> Unit, +fun Kind3RelayListView( + feedState: List, + postViewModel: Kind3RelayListViewModel, accountViewModel: AccountViewModel, - relayToAdd: String = "", + onClose: () -> Unit, nav: (String) -> Unit, + relayToAdd: String, ) { - val postViewModel: NewRelayListViewModel = viewModel() - val feedState by postViewModel.relays.collectAsStateWithLifecycle() - - LaunchedEffect(Unit) { postViewModel.load(accountViewModel.account) } - - Dialog( - onDismissRequest = onClose, - properties = DialogProperties(usePlatformDefaultWidth = false), - ) { - Scaffold( - topBar = { - TopAppBar( - title = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(modifier = StdHorzSpacer) - - Button( - onClick = { - postViewModel.deleteAll() - defaultRelays.forEach { postViewModel.addRelay(it) } - postViewModel.loadRelayDocuments() - }, - ) { - Text(stringResource(R.string.default_relays)) - } - - SaveButton( - onPost = { - postViewModel.create() - onClose() - }, - true, - ) - } - }, - navigationIcon = { - Spacer(modifier = DoubleHorzSpacer) - CloseButton( - onPress = { - postViewModel.clear() - onClose() - }, - ) - }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), - ) - }, - ) { pad -> - Column( - modifier = - Modifier.padding( - 16.dp, - pad.calculateTopPadding(), - 16.dp, - pad.calculateBottomPadding(), - ), - verticalArrangement = Arrangement.SpaceAround, - ) { - Row(modifier = Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) { - LazyColumn( - contentPadding = FeedPadding, - ) { - itemsIndexed(feedState, key = { _, item -> item.url }) { index, item -> - ServerConfig( - item, - onToggleDownload = { postViewModel.toggleDownload(it) }, - onToggleUpload = { postViewModel.toggleUpload(it) }, - onToggleFollows = { postViewModel.toggleFollows(it) }, - onTogglePrivateDMs = { postViewModel.toggleMessages(it) }, - onTogglePublicChats = { postViewModel.togglePublicChats(it) }, - onToggleGlobal = { postViewModel.toggleGlobal(it) }, - onToggleSearch = { postViewModel.toggleSearch(it) }, - onDelete = { postViewModel.deleteRelay(it) }, - accountViewModel = accountViewModel, - ) { - onClose() - nav(it) - } - } - } - } - - Spacer(modifier = StdVertSpacer) - - EditableServerConfig(relayToAdd) { postViewModel.addRelay(it) } - } - } - } -} - -@Composable -fun ServerConfigHeader() { - Column(Modifier.fillMaxWidth()) { - Row(verticalAlignment = Alignment.CenterVertically) { - Column(Modifier.weight(1f)) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = stringResource(R.string.relay_address), - modifier = Modifier.weight(1f), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - - Column(Modifier.weight(1.4f)) { - Row(verticalAlignment = Alignment.CenterVertically) { - Spacer(modifier = Modifier.size(30.dp)) - - Text( - text = stringResource(R.string.bytes), - maxLines = 1, - fontSize = Font14SP, - modifier = Modifier.weight(1.2f), - color = MaterialTheme.colorScheme.placeholderText, - ) - - Spacer(modifier = Modifier.size(5.dp)) - - Text( - text = stringResource(id = R.string.bytes), - maxLines = 1, - fontSize = Font14SP, - modifier = Modifier.weight(1.2f), - color = MaterialTheme.colorScheme.placeholderText, - ) - - Spacer(modifier = Modifier.size(5.dp)) - - Text( - text = stringResource(R.string.errors), - maxLines = 1, - fontSize = Font14SP, - modifier = Modifier.weight(1f), - color = MaterialTheme.colorScheme.placeholderText, - ) - - Spacer(modifier = Modifier.size(5.dp)) - - Text( - text = stringResource(R.string.spam), - maxLines = 1, - fontSize = Font14SP, - modifier = Modifier.weight(1f), - color = MaterialTheme.colorScheme.placeholderText, - ) - - Spacer(modifier = Modifier.size(2.dp)) + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + LazyColumn( + contentPadding = FeedPadding, + ) { + itemsIndexed(feedState, key = { _, item -> item.url }) { index, item -> + LoadRelayInfo( + item, + onToggleDownload = { postViewModel.toggleDownload(it) }, + onToggleUpload = { postViewModel.toggleUpload(it) }, + onToggleFollows = { postViewModel.toggleFollows(it) }, + onTogglePrivateDMs = { postViewModel.toggleMessages(it) }, + onTogglePublicChats = { postViewModel.togglePublicChats(it) }, + onToggleGlobal = { postViewModel.toggleGlobal(it) }, + onToggleSearch = { postViewModel.toggleSearch(it) }, + onDelete = { postViewModel.deleteRelay(it) }, + accountViewModel = accountViewModel, + ) { + onClose() + nav(it) } } } - - HorizontalDivider(thickness = DividerThickness) } + + Spacer(modifier = StdVertSpacer) + + Kind3RelayEditBox(relayToAdd) { postViewModel.addRelay(it) } } @Preview @Composable fun ServerConfigPreview() { - ServerConfigClickableLine( + ClickableRelayItem( loadProfilePicture = true, item = RelaySetupInfo( @@ -308,7 +162,7 @@ fun ServerConfigPreview() { } @Composable -fun ServerConfig( +fun LoadRelayInfo( item: RelaySetupInfo, onToggleDownload: (RelaySetupInfo) -> Unit, onToggleUpload: (RelaySetupInfo) -> Unit, @@ -339,7 +193,7 @@ fun ServerConfig( accountViewModel.settings.showProfilePictures.value } - ServerConfigClickableLine( + ClickableRelayItem( item = item, loadProfilePicture = automaticallyShowProfilePicture, onToggleDownload = onToggleDownload, @@ -353,7 +207,9 @@ fun ServerConfig( onClick = { accountViewModel.retrieveRelayDocument( item.url, - onInfo = { relayInfo = RelayInfoDialog(RelayBriefInfoCache.RelayBriefInfo(item.url), it) }, + onInfo = { + relayInfo = RelayInfoDialog(RelayBriefInfoCache.RelayBriefInfo(item.url), it) + }, onError = { url, errorCode, exceptionMessage -> val msg = when (errorCode) { @@ -363,18 +219,21 @@ fun ServerConfig( 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, @@ -394,7 +253,7 @@ fun ServerConfig( } @Composable -fun ServerConfigClickableLine( +fun ClickableRelayItem( item: RelaySetupInfo, loadProfilePicture: Boolean, onToggleDownload: (RelaySetupInfo) -> Unit, @@ -435,7 +294,7 @@ fun ServerConfigClickableLine( verticalAlignment = Alignment.CenterVertically, modifier = ReactionRowHeightChat.fillMaxWidth(), ) { - RenderActiveToggles( + ActiveToggles( item = item, onToggleFollows = onToggleFollows, onTogglePrivateDMs = onTogglePrivateDMs, @@ -449,7 +308,7 @@ fun ServerConfigClickableLine( verticalAlignment = Alignment.CenterVertically, modifier = ReactionRowHeightChat.fillMaxWidth(), ) { - RenderStatusRow( + StatusRow( item = item, onToggleDownload = onToggleDownload, onToggleUpload = onToggleUpload, @@ -465,7 +324,7 @@ fun ServerConfigClickableLine( @Composable @OptIn(ExperimentalFoundationApi::class) -private fun RenderStatusRow( +private fun StatusRow( item: RelaySetupInfo, onToggleDownload: (RelaySetupInfo) -> Unit, onToggleUpload: (RelaySetupInfo) -> Unit, @@ -617,7 +476,7 @@ private fun RenderStatusRow( @Composable @OptIn(ExperimentalFoundationApi::class) -private fun RenderActiveToggles( +private fun ActiveToggles( item: RelaySetupInfo, onToggleFollows: (RelaySetupInfo) -> Unit, onTogglePrivateDMs: (RelaySetupInfo) -> Unit, @@ -846,7 +705,7 @@ private fun FirstLine( } @Composable -fun EditableServerConfig( +fun Kind3RelayEditBox( relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Unit, ) { @@ -902,7 +761,14 @@ fun EditableServerConfig( onClick = { if (url.isNotBlank() && url != "/") { val addedWSS = RelayUrlFormatter.normalize(url) - onNewRelay(RelaySetupInfo(addedWSS, read, write, feedTypes = FeedType.values().toSet())) + onNewRelay( + RelaySetupInfo( + addedWSS, + read, + write, + feedTypes = FeedType.values().toSet(), + ), + ) url = "" write = true read = true @@ -923,21 +789,3 @@ fun EditableServerConfig( } } } - -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/relays/Kind3RelayListViewModel.kt similarity index 98% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListViewModel.kt index a86574f67..9b417a817 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListViewModel.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.actions +package com.vitorpamplona.amethyst.ui.actions.relays import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -36,7 +36,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -class NewRelayListViewModel : ViewModel() { +class Kind3RelayListViewModel : ViewModel() { private lateinit var account: Account private val _relays = MutableStateFlow>(emptyList()) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt index 2a1e6b20d..7a7846a41 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.actions.updated +import com.vitorpamplona.amethyst.ui.actions.relays.updated import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ChatroomKeyable diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt index a6ef95299..87cc2c66a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.actions.updated +import com.vitorpamplona.amethyst.ui.actions.relays.updated import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ChatroomKeyable import com.vitorpamplona.quartz.events.PrivateDmEvent diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 1e5689fb8..8a7336eaa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -88,7 +88,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.service.relays.RelayPoolStatus -import com.vitorpamplona.amethyst.ui.actions.NewRelayListView +import com.vitorpamplona.amethyst.ui.actions.relays.AllRelayListView import com.vitorpamplona.amethyst.ui.components.ClickableText import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage @@ -566,7 +566,7 @@ fun ListContent( } if (wantsToEditRelays) { - NewRelayListView({ wantsToEditRelays = false }, accountViewModel, nav = nav) + AllRelayListView({ wantsToEditRelays = false }, accountViewModel = accountViewModel, nav = nav) } if (backupDialogOpen) { AccountBackupDialog(accountViewModel, onClose = { backupDialogOpen = false }) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt index 28ee3c25d..7c64e9f5a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt @@ -43,7 +43,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.ThemeType -import com.vitorpamplona.amethyst.ui.actions.DMRelayListView +import com.vitorpamplona.amethyst.ui.actions.relays.AddDMRelayListDialog import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -186,7 +186,7 @@ fun AddInboxRelayForDMCard( var wantsToEditRelays by remember { mutableStateOf(false) } if (wantsToEditRelays) { - DMRelayListView({ wantsToEditRelays = false }, accountViewModel, nav = nav) + AddDMRelayListDialog({ wantsToEditRelays = false }, accountViewModel, nav = nav) } Button( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt index ea134909e..5bef0fcdb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt @@ -45,7 +45,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.RelayBriefInfoCache -import com.vitorpamplona.amethyst.ui.actions.NewRelayListView +import com.vitorpamplona.amethyst.ui.actions.relays.AllRelayListView import com.vitorpamplona.amethyst.ui.components.ShowMoreButton import com.vitorpamplona.amethyst.ui.note.AddRelayButton import com.vitorpamplona.amethyst.ui.note.RemoveRelayButton @@ -167,7 +167,7 @@ private fun RelayOptionsAction( var wantsToAddRelay by remember { mutableStateOf("") } if (wantsToAddRelay.isNotEmpty()) { - NewRelayListView({ wantsToAddRelay = "" }, accountViewModel, wantsToAddRelay, nav = nav) + AllRelayListView({ wantsToAddRelay = "" }, wantsToAddRelay, accountViewModel, 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 8cb4ba12e..dafa52a53 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 @@ -37,7 +37,7 @@ import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.RelayInfo import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.UserState -import com.vitorpamplona.amethyst.ui.actions.NewRelayListView +import com.vitorpamplona.amethyst.ui.actions.relays.AllRelayListView import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.note.RelayCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -132,7 +132,7 @@ fun RelayFeedView( var wantsToAddRelay by remember { mutableStateOf("") } if (wantsToAddRelay.isNotEmpty()) { - NewRelayListView({ wantsToAddRelay = "" }, accountViewModel, wantsToAddRelay, nav = nav) + AllRelayListView({ wantsToAddRelay = "" }, wantsToAddRelay, accountViewModel, nav = nav) } RefresheableBox(viewModel, enablePullRefresh) {