Code review:
- Gate the new notificationsFollowing / notificationsEveryone fan-out in updateFeedsWith / deleteNotes on splitNotificationsEnabled.value so the default-off case stops doing two extra filter scans per event bundle. - Lazy-init NotificationFeedFilter.overrideFollowLists so the SharingStarted.Eagerly topNavFilter pipeline only starts when the split UI actually opens the filter. - Switch SplitNotificationsScaffold to rememberForeverPagerState so the active tab survives recomposition, matching HomeScreen/DiscoverScreen. - Promote the duplicated "Notification" last-read key string to a NOTIFICATION_LAST_READ_KEY const. - Move the opaque-background fix into SummaryBar itself so callers don't need a band-aid Modifier.background on their topBar Column. - Trim narrating / WHAT comments; keep only the deep-link scroll WHY.
This commit is contained in:
@@ -83,6 +83,7 @@ object PagerStateKeys {
|
|||||||
const val HOME_SCREEN = "PagerHome"
|
const val HOME_SCREEN = "PagerHome"
|
||||||
const val DISCOVER_SCREEN = "PagerDiscover"
|
const val DISCOVER_SCREEN = "PagerDiscover"
|
||||||
const val POLLS_SCREEN = "PagerPolls"
|
const val POLLS_SCREEN = "PagerPolls"
|
||||||
|
const val NOTIFICATION_SCREEN = "PagerNotification"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+8
-9
@@ -109,11 +109,6 @@ class AccountFeedContentStates(
|
|||||||
val articlesFeed = FeedContentState(ArticlesFeedFilter(account), scope, LocalCache)
|
val articlesFeed = FeedContentState(ArticlesFeedFilter(account), scope, LocalCache)
|
||||||
|
|
||||||
val notifications = CardFeedContentState(NotificationFeedFilter(account), scope)
|
val notifications = CardFeedContentState(NotificationFeedFilter(account), scope)
|
||||||
|
|
||||||
// Split-notifications mode (Issue #197): when [AccountSettings.splitNotificationsEnabled]
|
|
||||||
// is on, the screen renders these two pinned-mode feeds in tabs instead of [notifications].
|
|
||||||
// Following = kind:3 follow list members only; Everyone = global. Both are always
|
|
||||||
// constructed so updateFeedsWith can fan out incoming events without rebuilding state.
|
|
||||||
val notificationsFollowing = CardFeedContentState(NotificationFeedFilter(account, TopFilter.AllFollows), scope)
|
val notificationsFollowing = CardFeedContentState(NotificationFeedFilter(account, TopFilter.AllFollows), scope)
|
||||||
val notificationsEveryone = CardFeedContentState(NotificationFeedFilter(account, TopFilter.Global), scope)
|
val notificationsEveryone = CardFeedContentState(NotificationFeedFilter(account, TopFilter.Global), scope)
|
||||||
|
|
||||||
@@ -192,8 +187,10 @@ class AccountFeedContentStates(
|
|||||||
articlesFeed.updateFeedWith(newNotes)
|
articlesFeed.updateFeedWith(newNotes)
|
||||||
|
|
||||||
notifications.updateFeedWith(newNotes)
|
notifications.updateFeedWith(newNotes)
|
||||||
notificationsFollowing.updateFeedWith(newNotes)
|
if (account.settings.splitNotificationsEnabled.value) {
|
||||||
notificationsEveryone.updateFeedWith(newNotes)
|
notificationsFollowing.updateFeedWith(newNotes)
|
||||||
|
notificationsEveryone.updateFeedWith(newNotes)
|
||||||
|
}
|
||||||
notificationSummary.invalidateInsertData(newNotes)
|
notificationSummary.invalidateInsertData(newNotes)
|
||||||
|
|
||||||
drafts.updateFeedWith(newNotes)
|
drafts.updateFeedWith(newNotes)
|
||||||
@@ -242,8 +239,10 @@ class AccountFeedContentStates(
|
|||||||
articlesFeed.deleteFromFeed(newNotes)
|
articlesFeed.deleteFromFeed(newNotes)
|
||||||
|
|
||||||
notifications.deleteFromFeed(newNotes)
|
notifications.deleteFromFeed(newNotes)
|
||||||
notificationsFollowing.deleteFromFeed(newNotes)
|
if (account.settings.splitNotificationsEnabled.value) {
|
||||||
notificationsEveryone.deleteFromFeed(newNotes)
|
notificationsFollowing.deleteFromFeed(newNotes)
|
||||||
|
notificationsEveryone.deleteFromFeed(newNotes)
|
||||||
|
}
|
||||||
notificationSummary.invalidateInsertData(newNotes)
|
notificationSummary.invalidateInsertData(newNotes)
|
||||||
|
|
||||||
drafts.deleteFromFeed(newNotes)
|
drafts.deleteFromFeed(newNotes)
|
||||||
|
|||||||
+4
-6
@@ -81,6 +81,7 @@ import com.vitorpamplona.amethyst.ui.note.showAmount
|
|||||||
import com.vitorpamplona.amethyst.ui.note.showAmountInteger
|
import com.vitorpamplona.amethyst.ui.note.showAmountInteger
|
||||||
import com.vitorpamplona.amethyst.ui.screen.UiSettingsState
|
import com.vitorpamplona.amethyst.ui.screen.UiSettingsState
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CombinedZap
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CombinedZap
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NOTIFICATION_LAST_READ_KEY
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSync
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSync
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||||
@@ -319,16 +320,13 @@ class AccountViewModel(
|
|||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val notificationHasNewItems =
|
val notificationHasNewItems =
|
||||||
// Issue #197: when split-notifications is ON, the bottom-bar dot must glow only
|
// When split-notifications is on, the badge tracks only the Following feed.
|
||||||
// for items in the Following tab. When OFF, fall back to the user-selected single
|
|
||||||
// feed. Switch sources on the toggle flow so changing the setting takes effect
|
|
||||||
// without a restart.
|
|
||||||
account.settings.splitNotificationsEnabled
|
account.settings.splitNotificationsEnabled
|
||||||
.flatMapLatest { isSplit ->
|
.flatMapLatest { isSplit ->
|
||||||
val source =
|
val source =
|
||||||
if (isSplit) feedStates.notificationsFollowing else feedStates.notifications
|
if (isSplit) feedStates.notificationsFollowing else feedStates.notifications
|
||||||
combineTransform(
|
combineTransform(
|
||||||
account.loadLastReadFlow("Notification"),
|
account.loadLastReadFlow(NOTIFICATION_LAST_READ_KEY),
|
||||||
source.feedContent
|
source.feedContent
|
||||||
.flatMapLatest {
|
.flatMapLatest {
|
||||||
if (it is CardFeedState.Loaded) {
|
if (it is CardFeedState.Loaded) {
|
||||||
@@ -347,7 +345,7 @@ class AccountViewModel(
|
|||||||
} else {
|
} else {
|
||||||
feedStates.notifications
|
feedStates.notifications
|
||||||
}
|
}
|
||||||
val lastRead = account.loadLastReadFlow("Notification").value
|
val lastRead = account.loadLastReadFlow(NOTIFICATION_LAST_READ_KEY).value
|
||||||
val cards = source.feedContent.value
|
val cards = source.feedContent.value
|
||||||
if (cards is CardFeedState.Loaded) {
|
if (cards is CardFeedState.Loaded) {
|
||||||
val newestItemCreatedAt =
|
val newestItemCreatedAt =
|
||||||
|
|||||||
+9
-19
@@ -20,11 +20,9 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.PagerState
|
import androidx.compose.foundation.pager.PagerState
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SecondaryTabRow
|
import androidx.compose.material3.SecondaryTabRow
|
||||||
import androidx.compose.material3.Tab
|
import androidx.compose.material3.Tab
|
||||||
@@ -33,15 +31,16 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
||||||
import com.vitorpamplona.amethyst.ui.components.SelectNotificationProvider
|
import com.vitorpamplona.amethyst.ui.components.SelectNotificationProvider
|
||||||
|
import com.vitorpamplona.amethyst.ui.feeds.PagerStateKeys
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
|
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop
|
import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverLazyListState
|
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverLazyListState
|
||||||
|
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
|
||||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
@@ -51,6 +50,8 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
const val NOTIFICATION_LAST_READ_KEY = "Notification"
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NotificationScreen(
|
fun NotificationScreen(
|
||||||
scrollToEventId: String? = null,
|
scrollToEventId: String? = null,
|
||||||
@@ -124,10 +125,7 @@ private fun SingleNotificationsScaffold(
|
|||||||
DisappearingScaffold(
|
DisappearingScaffold(
|
||||||
isInvertedLayout = false,
|
isInvertedLayout = false,
|
||||||
topBar = {
|
topBar = {
|
||||||
// DisappearingScaffold layers content under a translating topBar; without an
|
Column {
|
||||||
// opaque background, feed items scroll visibly through the SummaryBar gap
|
|
||||||
// between NotificationTopBar and the divider.
|
|
||||||
Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
|
|
||||||
NotificationTopBar(accountViewModel, nav, showSpinner = true)
|
NotificationTopBar(accountViewModel, nav, showSpinner = true)
|
||||||
SummaryBar(state = notifSummaryState)
|
SummaryBar(state = notifSummaryState)
|
||||||
}
|
}
|
||||||
@@ -163,18 +161,13 @@ private fun SplitNotificationsScaffold(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val pagerState = rememberPagerState(pageCount = { 2 })
|
val pagerState = rememberForeverPagerState(key = PagerStateKeys.NOTIFICATION_SCREEN) { 2 }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
DisappearingScaffold(
|
DisappearingScaffold(
|
||||||
isInvertedLayout = false,
|
isInvertedLayout = false,
|
||||||
topBar = {
|
topBar = {
|
||||||
// Mirror HomeScreen: the TabRow lives in the topBar Column so it inherits
|
Column {
|
||||||
// the scaffold's status-bar inset handling and scrolls together with the
|
|
||||||
// title + summary instead of floating into the status bar area.
|
|
||||||
// Opaque background hides feed items that scroll under the topBar through
|
|
||||||
// the SummaryBar gap (which has no container of its own).
|
|
||||||
Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
|
|
||||||
NotificationTopBar(accountViewModel, nav, showSpinner = false)
|
NotificationTopBar(accountViewModel, nav, showSpinner = false)
|
||||||
SummaryBar(state = notifSummaryState)
|
SummaryBar(state = notifSummaryState)
|
||||||
SecondaryTabRow(
|
SecondaryTabRow(
|
||||||
@@ -240,7 +233,7 @@ private fun SingleNotificationsBody(
|
|||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
listState = listState,
|
listState = listState,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
routeForLastRead = "Notification",
|
routeForLastRead = NOTIFICATION_LAST_READ_KEY,
|
||||||
scrollToEventId = scrollToEventId,
|
scrollToEventId = scrollToEventId,
|
||||||
headerContent = { ObserveInboxRelayListAndDisplayIfNotFound(accountViewModel, nav) },
|
headerContent = { ObserveInboxRelayListAndDisplayIfNotFound(accountViewModel, nav) },
|
||||||
)
|
)
|
||||||
@@ -306,10 +299,7 @@ private fun NotificationPagerPage(
|
|||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
listState = listState,
|
listState = listState,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
// Both tabs share the "Notification" last-read marker (Section H #7): the
|
routeForLastRead = NOTIFICATION_LAST_READ_KEY,
|
||||||
// unread indicator is gated to the Following tab elsewhere, so a single
|
|
||||||
// marker keeps migration trivial and avoids a stale marker per tab.
|
|
||||||
routeForLastRead = "Notification",
|
|
||||||
scrollToEventId = scrollToEventId,
|
scrollToEventId = scrollToEventId,
|
||||||
headerContent = { ObserveInboxRelayListAndDisplayIfNotFound(accountViewModel, nav) },
|
headerContent = { ObserveInboxRelayListAndDisplayIfNotFound(accountViewModel, nav) },
|
||||||
)
|
)
|
||||||
|
|||||||
+19
-13
@@ -25,7 +25,9 @@ import androidx.compose.animation.expandVertically
|
|||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -46,21 +48,25 @@ import com.vitorpamplona.amethyst.ui.theme.chartStyle
|
|||||||
fun SummaryBar(state: NotificationSummaryState) {
|
fun SummaryBar(state: NotificationSummaryState) {
|
||||||
var showChart by remember { mutableStateOf(false) }
|
var showChart by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
UserReactionsRow(state) { showChart = !showChart }
|
// Opaque background: DisappearingScaffold layers feed content under the topBar
|
||||||
|
// on scroll, and SummaryBar otherwise has no container of its own.
|
||||||
|
Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
|
||||||
|
UserReactionsRow(state) { showChart = !showChart }
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = showChart,
|
visible = showChart,
|
||||||
enter = slideInVertically() + expandVertically(),
|
enter = slideInVertically() + expandVertically(),
|
||||||
exit = slideOutVertically() + shrinkVertically(),
|
exit = slideOutVertically() + shrinkVertically(),
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.padding(vertical = 0.dp, horizontal = 20.dp)
|
|
||||||
.clickable(onClick = { showChart = !showChart }),
|
|
||||||
) {
|
) {
|
||||||
ProvideVicoTheme(MaterialTheme.colorScheme.chartStyle) {
|
Row(
|
||||||
ObserveAndShowChart(state)
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(vertical = 0.dp, horizontal = 20.dp)
|
||||||
|
.clickable(onClick = { showChart = !showChart }),
|
||||||
|
) {
|
||||||
|
ProvideVicoTheme(MaterialTheme.colorScheme.chartStyle) {
|
||||||
|
ObserveAndShowChart(state)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-2
@@ -52,8 +52,6 @@ fun NotificationTopBar(
|
|||||||
onChange = accountViewModel.account.settings::changeDefaultNotificationFollowList,
|
onChange = accountViewModel.account.settings::changeDefaultNotificationFollowList,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Split-notifications (Issue #197): tabs replace the list-filter spinner, so
|
|
||||||
// render a plain title in the top bar's content slot.
|
|
||||||
Text(text = stringRes(R.string.route_notifications))
|
Text(text = stringRes(R.string.route_notifications))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-6
@@ -81,13 +81,12 @@ class NotificationFeedFilter(
|
|||||||
val account: Account,
|
val account: Account,
|
||||||
val modeOverride: TopFilter? = null,
|
val modeOverride: TopFilter? = null,
|
||||||
) : AdditiveFeedFilter<Note>() {
|
) : AdditiveFeedFilter<Note>() {
|
||||||
// When [modeOverride] is set (split-notifications tabs), build a dedicated
|
// Pin to modeOverride for split-tab mode; otherwise follow the spinner.
|
||||||
// IFeedTopNavFilter flow so this filter is independent of the user-controlled
|
// Lazy so the eagerly-collected topNavFilter pipeline is only built when
|
||||||
// [Account.liveNotificationFollowLists]. The Following tab pins AllFollows; the
|
// the split UI actually opens this filter.
|
||||||
// Everyone tab pins Global. When the override is null we fall back to the
|
private val overrideFollowLists: StateFlow<IFeedTopNavFilter>? by lazy {
|
||||||
// shared, spinner-driven flow used by the single-feed layout.
|
|
||||||
private val overrideFollowLists: StateFlow<IFeedTopNavFilter>? =
|
|
||||||
modeOverride?.let { account.topNavFilterFlow(MutableStateFlow(it)) }
|
modeOverride?.let { account.topNavFilterFlow(MutableStateFlow(it)) }
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val ADDRESSABLE_KINDS =
|
val ADDRESSABLE_KINDS =
|
||||||
|
|||||||
Reference in New Issue
Block a user