fix(interest-sets): consume kind 30015 locally + empty-state polish

- LocalCache.consume dispatcher now handles InterestSetEvent via
  consumeBaseReplaceable, so the event created by the user is stored
  and flows through newEventBundles → interestSets.newNotes → listFeedFlow
  refresh. Without this, the just-signed event sat in the sendMyPublicAndPrivateOutbox
  call but the UI never saw the update.
- List screen: icon + centered text for the empty state and dividers
  between rows.
This commit is contained in:
Claude
2026-04-20 14:11:06 +00:00
parent 198e950f37
commit 4c82011a25
2 changed files with 38 additions and 9 deletions
@@ -153,6 +153,7 @@ import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFee
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
@@ -2642,6 +2643,7 @@ object LocalCache : ILocalCache, ICacheProvider {
is InteractiveStoryPrologueEvent -> consumeBaseReplaceable(event, relay, wasVerified) is InteractiveStoryPrologueEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is InteractiveStorySceneEvent -> consumeBaseReplaceable(event, relay, wasVerified) is InteractiveStorySceneEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is InteractiveStoryReadingStateEvent -> consumeBaseReplaceable(event, relay, wasVerified) is InteractiveStoryReadingStateEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is InterestSetEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is LabeledBookmarkListEvent -> consumeBaseReplaceable(event, relay, wasVerified) is LabeledBookmarkListEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is LiveActivitiesEvent -> consume(event, relay, wasVerified) is LiveActivitiesEvent -> consume(event, relay, wasVerified)
is LiveActivitiesChatMessageEvent -> consume(event, relay, wasVerified) is LiveActivitiesChatMessageEvent -> consume(event, relay, wasVerified)
@@ -24,13 +24,17 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.PlaylistAdd import androidx.compose.material.icons.automirrored.filled.PlaylistAdd
import androidx.compose.material.icons.outlined.Tag
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@@ -39,6 +43,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
@@ -46,6 +52,8 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
@Composable @Composable
fun ListOfInterestSetsScreen( fun ListOfInterestSetsScreen(
@@ -71,19 +79,15 @@ fun ListOfInterestSetsScreen(
).fillMaxHeight(), ).fillMaxHeight(),
) { ) {
if (sets.isEmpty()) { if (sets.isEmpty()) {
Column( EmptyInterestSets()
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(stringRes(R.string.interest_sets_empty))
}
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = FeedPadding,
) { ) {
items(sets, key = { it.identifier }) { set -> itemsIndexed(sets, key = { _, it -> it.identifier }) { _, set ->
InterestSetItem( InterestSetItem(
modifier = Modifier.fillMaxSize().animateItem(),
interestSet = set, interestSet = set,
onClick = { nav.nav(Route.InterestSetView(set.identifier)) }, onClick = { nav.nav(Route.InterestSetView(set.identifier)) },
onRename = { nav.nav(Route.InterestSetMetadataEdit(set.identifier)) }, onRename = { nav.nav(Route.InterestSetMetadataEdit(set.identifier)) },
@@ -105,6 +109,7 @@ fun ListOfInterestSetsScreen(
} }
}, },
) )
HorizontalDivider(thickness = DividerThickness)
} }
} }
} }
@@ -112,6 +117,28 @@ fun ListOfInterestSetsScreen(
} }
} }
@Composable
private fun EmptyInterestSets() {
Column(
modifier = Modifier.fillMaxSize().padding(32.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = Icons.Outlined.Tag,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(64.dp),
)
Text(
text = stringRes(R.string.interest_sets_empty),
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
)
}
}
@Composable @Composable
fun InterestSetFab(onAdd: () -> Unit) { fun InterestSetFab(onAdd: () -> Unit) {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(