Adds a missing outbox popup on posting new notes

This commit is contained in:
Vitor Pamplona
2025-10-28 19:09:24 -04:00
parent 09e4b2ea44
commit 00eb6ee3f1
6 changed files with 288 additions and 0 deletions

View File

@@ -93,6 +93,17 @@ class Nip65RelayListState(
emptySet(), emptySet(),
) )
val outboxFlowNoDefaults =
getNIP65RelayListFlow()
.map { normalizeNIP65WriteRelayListNoDefaults(it.note) }
.onStart { emit(normalizeNIP65WriteRelayListNoDefaults(nip65ListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
val inboxFlowNoDefaults = val inboxFlowNoDefaults =
getNIP65RelayListFlow() getNIP65RelayListFlow()
.map { normalizeNIP65ReadRelayListNoDefaults(it.note) } .map { normalizeNIP65ReadRelayListNoDefaults(it.note) }

View File

@@ -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))
}
}
}
}
}

View File

@@ -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),
)
}
}
}

View File

@@ -221,6 +221,8 @@ private fun NewPostScreenBody(
modifier = modifier =
Modifier.fillMaxSize(), Modifier.fillMaxSize(),
) { ) {
ObserveInboxRelayListAndDisplayIfNotFound(accountViewModel, nav)
Row( Row(
modifier = modifier =
Modifier Modifier

View File

@@ -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( fun LazyListScope.renderNip65HomeItems(
feedState: List<BasicRelaySetupInfo>, feedState: List<BasicRelaySetupInfo>,
postViewModel: Nip65RelayListViewModel, postViewModel: Nip65RelayListViewModel,

View File

@@ -1146,6 +1146,12 @@
<string name="search_relays_not_found_editing">Insert between 13 relays to use when searching for content or tagging users. Make sure your chosen relays implement NIP-50</string> <string name="search_relays_not_found_editing">Insert between 13 relays to use when searching for content or tagging users. Make sure your chosen relays implement NIP-50</string>
<string name="search_relays_not_found_examples">Good options are:\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com</string> <string name="search_relays_not_found_examples">Good options are:\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com</string>
<string name="outbox_relays_title">Outbox Relays</string>
<string name="outbox_relays_not_found">Set up your Public Outbox relays to post</string>
<string name="outbox_relays_not_found_description">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. </string>
<string name="outbox_relays_not_found_editing">Insert between 13 relays that receive your posts. Make sure they don\'t require payment if you are not paying to insert</string>
<string name="outbox_relays_not_found_examples">Good options are:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social</string>
<string name="inbox_relays_title">Inbox Relays</string> <string name="inbox_relays_title">Inbox Relays</string>
<string name="inbox_relays_not_found">Set up your Public Inbox relays to receive notifications</string> <string name="inbox_relays_not_found">Set up your Public Inbox relays to receive notifications</string>
<string name="inbox_relays_not_found_description">Creating a relay list specifically designed to receive notifications is crucial for your Nostr experience. </string> <string name="inbox_relays_not_found_description">Creating a relay list specifically designed to receive notifications is crucial for your Nostr experience. </string>