diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt index 7444f25c6..bdeecd489 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt @@ -93,6 +93,17 @@ class Nip65RelayListState( emptySet(), ) + val outboxFlowNoDefaults = + getNIP65RelayListFlow() + .map { normalizeNIP65WriteRelayListNoDefaults(it.note) } + .onStart { emit(normalizeNIP65WriteRelayListNoDefaults(nip65ListNote)) } + .flowOn(Dispatchers.IO) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + val inboxFlowNoDefaults = getNIP65RelayListFlow() .map { normalizeNIP65ReadRelayListNoDefaults(it.note) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayCard.kt new file mode 100644 index 000000000..69682a082 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayCard.kt @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2025 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.screen.loggedIn.home + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.AddInboxRelayCard +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.BigPadding +import com.vitorpamplona.amethyst.ui.theme.StdPadding +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn +import com.vitorpamplona.amethyst.ui.theme.imageModifier + +@Preview +@Composable +fun AddOutboxRelayCardPreview() { + ThemeComparisonColumn { + AddInboxRelayCard( + accountViewModel = mockAccountViewModel(), + nav = EmptyNav, + ) + } +} + +@Composable +fun ObserveInboxRelayListAndDisplayIfNotFound( + accountViewModel: AccountViewModel, + nav: INav, +) { + val outboxRelayList by accountViewModel.account.nip65RelayList.outboxFlowNoDefaults + .collectAsStateWithLifecycle() + + if (outboxRelayList.isEmpty()) { + AddOutboxRelayCard( + accountViewModel = accountViewModel, + nav = nav, + ) + } +} + +@Composable +fun AddOutboxRelayCard( + accountViewModel: AccountViewModel, + nav: INav, +) { + Column(modifier = StdPadding) { + Card( + modifier = MaterialTheme.colorScheme.imageModifier, + ) { + Column( + modifier = BigPadding, + ) { + // Title + Text( + text = stringRes(id = R.string.outbox_relays_not_found), + style = + TextStyle( + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + ), + ) + + Spacer(modifier = StdVertSpacer) + + Text( + text = stringRes(id = R.string.outbox_relays_not_found_description), + ) + + Spacer(modifier = StdVertSpacer) + + var wantsToEditRelays by remember { mutableStateOf(false) } + if (wantsToEditRelays) { + AddOutboxRelayListDialog( + { wantsToEditRelays = false }, + accountViewModel, + nav = nav, + ) + } + + Button( + onClick = { + wantsToEditRelays = true + }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(text = stringRes(id = R.string.dm_relays_not_found_create_now)) + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayListDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayListDialog.kt new file mode 100644 index 000000000..126b80477 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayListDialog.kt @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2025 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.screen.loggedIn.home + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +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.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +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.components.SetDialogToEdgeToEdge +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.Nip65OutboxRelayList +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.Nip65RelayListViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.amethyst.ui.theme.imageModifier + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddOutboxRelayListDialog( + onClose: () -> Unit, + accountViewModel: AccountViewModel, + nav: INav, +) { + val postViewModel: Nip65RelayListViewModel = viewModel() + + postViewModel.init(accountViewModel) + + LaunchedEffect(postViewModel, accountViewModel.account) { + postViewModel.load() + } + + Dialog( + onDismissRequest = onClose, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + SetDialogToEdgeToEdge() + Scaffold( + topBar = { + SavingTopBar( + titleRes = R.string.outbox_relays_title, + onCancel = { + postViewModel.clear() + onClose() + }, + onPost = { + postViewModel.create() + onClose() + }, + ) + }, + ) { pad -> + Column( + modifier = + Modifier.padding( + 16.dp, + pad.calculateTopPadding(), + 16.dp, + pad.calculateBottomPadding(), + ), + verticalArrangement = Arrangement.SpaceAround, + ) { + Explanation(postViewModel) + + Nip65OutboxRelayList(postViewModel, accountViewModel, onClose, nav) + } + } + } +} + +@Composable +private fun Explanation(postViewModel: Nip65RelayListViewModel) { + Card(modifier = MaterialTheme.colorScheme.imageModifier) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = stringRes(id = R.string.outbox_relays_not_found_editing), + ) + + Spacer(modifier = StdVertSpacer) + + Text( + text = stringRes(id = R.string.outbox_relays_not_found_examples), + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 7d5df8868..2bb6bd098 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -221,6 +221,8 @@ private fun NewPostScreenBody( modifier = Modifier.fillMaxSize(), ) { + ObserveInboxRelayListAndDisplayIfNotFound(accountViewModel, nav) + Row( modifier = Modifier diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt index 5761fdf87..baea4874a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt @@ -81,6 +81,26 @@ fun Nip65InboxRelayList( } } +@Composable +fun Nip65OutboxRelayList( + postViewModel: Nip65RelayListViewModel, + accountViewModel: AccountViewModel, + onClose: () -> Unit, + nav: INav, +) { + val newNav = rememberExtendedNav(nav, onClose) + + val homeFeedState by postViewModel.homeRelays.collectAsStateWithLifecycle() + + Row(verticalAlignment = Alignment.CenterVertically) { + LazyColumn( + contentPadding = FeedPadding, + ) { + renderNip65HomeItems(homeFeedState, postViewModel, accountViewModel, newNav) + } + } +} + fun LazyListScope.renderNip65HomeItems( feedState: List, postViewModel: Nip65RelayListViewModel, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 6ff72c3d3..99c201726 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1146,6 +1146,12 @@ Insert between 1–3 relays to use when searching for content or tagging users. Make sure your chosen relays implement NIP-50 Good options are:\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com + Outbox Relays + Set up your Public Outbox relays to post + Creating a relay list specifically designed to receive your content is crucial for your Nostr experience and the only way your followers can find you. + Insert between 1–3 relays that receive your posts. Make sure they don\'t require payment if you are not paying to insert + Good options are:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social + Inbox Relays Set up your Public Inbox relays to receive notifications Creating a relay list specifically designed to receive notifications is crucial for your Nostr experience.