Merge pull request #2066 from vitorpamplona/claude/zap-notification-routing-MBmaw
Add scroll-to-event functionality for push notifications
This commit is contained in:
+9
-4
@@ -60,6 +60,7 @@ import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
private const val TAG = "EventNotificationConsumer"
|
||||
private const val ACCOUNT_QUERY_PARAM = "?account="
|
||||
private const val SCROLL_TO_QUERY_PARAM = "&scrollTo="
|
||||
|
||||
class EventNotificationConsumer(
|
||||
private val applicationContext: Context,
|
||||
@@ -455,7 +456,8 @@ class EventNotificationConsumer(
|
||||
"notifications$ACCOUNT_QUERY_PARAM" +
|
||||
account.signer.pubKey
|
||||
.hexToByteArray()
|
||||
.toNpub()
|
||||
.toNpub() +
|
||||
SCROLL_TO_QUERY_PARAM + event.id
|
||||
|
||||
Log.d(TAG) { "Notify ${event.id} $content $title $noteUri" }
|
||||
|
||||
@@ -490,7 +492,8 @@ class EventNotificationConsumer(
|
||||
"notifications$ACCOUNT_QUERY_PARAM" +
|
||||
account.signer.pubKey
|
||||
.hexToByteArray()
|
||||
.toNpub()
|
||||
.toNpub() +
|
||||
SCROLL_TO_QUERY_PARAM + event.id
|
||||
|
||||
Log.d(TAG) { "Notify ${event.id} $title $noteUri" }
|
||||
|
||||
@@ -568,7 +571,8 @@ class EventNotificationConsumer(
|
||||
"notifications$ACCOUNT_QUERY_PARAM" +
|
||||
account.signer.pubKey
|
||||
.hexToByteArray()
|
||||
.toNpub()
|
||||
.toNpub() +
|
||||
SCROLL_TO_QUERY_PARAM + event.id
|
||||
|
||||
notificationManager()
|
||||
.sendReactionNotification(
|
||||
@@ -600,7 +604,8 @@ class EventNotificationConsumer(
|
||||
"notifications$ACCOUNT_QUERY_PARAM" +
|
||||
account.signer.pubKey
|
||||
.hexToByteArray()
|
||||
.toNpub()
|
||||
.toNpub() +
|
||||
SCROLL_TO_QUERY_PARAM + event.id
|
||||
|
||||
notificationManager()
|
||||
.sendChessNotification(
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING
|
||||
import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia
|
||||
import com.vitorpamplona.amethyst.ui.navigation.findParameterValue
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
|
||||
@@ -129,7 +130,8 @@ fun uriToRoute(
|
||||
account: Account,
|
||||
): Route? {
|
||||
if (isNotificationRoute(uri)) {
|
||||
return Route.Notification
|
||||
val scrollTo = runCatching { java.net.URI(uri.removePrefix("nostr:")).findParameterValue("scrollTo") }.getOrNull()
|
||||
return Route.Notification(scrollToEventId = scrollTo)
|
||||
}
|
||||
if (isHashtagRoute(uri)) {
|
||||
return Route.Hashtag(uri.removePrefix("nostr:").removePrefix("hashtag?id=").lowercase())
|
||||
|
||||
@@ -178,7 +178,7 @@ fun BuildNavigation(
|
||||
composable<Route.Message> { MessagesScreen(accountViewModel, nav) }
|
||||
composable<Route.Video> { VideoScreen(accountViewModel, nav) }
|
||||
composable<Route.Discover> { DiscoverScreen(accountViewModel, nav) }
|
||||
composable<Route.Notification> { NotificationScreen(accountViewModel, nav) }
|
||||
composableArgs<Route.Notification> { NotificationScreen(it.scrollToEventId, accountViewModel, nav) }
|
||||
composableFromEnd<Route.Polls> { PollsScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.Pictures> { PicturesScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.Shorts> { ShortsScreen(accountViewModel, nav) }
|
||||
|
||||
+1
-1
@@ -44,5 +44,5 @@ val bottomNavigationItems =
|
||||
BottomBarRoute(Route.Message, R.drawable.ic_dm, R.string.route_messages),
|
||||
BottomBarRoute(Route.Video, R.drawable.ic_video, R.string.route_video),
|
||||
BottomBarRoute(Route.Discover, R.drawable.ic_sensors, R.string.route_discover),
|
||||
BottomBarRoute(Route.Notification, R.drawable.ic_notifications, R.string.route_notifications),
|
||||
BottomBarRoute(Route.Notification(), R.drawable.ic_notifications, R.string.route_notifications),
|
||||
)
|
||||
|
||||
@@ -39,7 +39,9 @@ sealed class Route {
|
||||
|
||||
@Serializable object Discover : Route()
|
||||
|
||||
@Serializable object Notification : Route()
|
||||
@Serializable data class Notification(
|
||||
val scrollToEventId: String? = null,
|
||||
) : Route()
|
||||
|
||||
@Serializable object Polls : Route()
|
||||
|
||||
|
||||
+1
-1
@@ -366,7 +366,7 @@ class AccountViewModel(
|
||||
mapOf(
|
||||
Route.Home to homeHasNewItemsFlow,
|
||||
Route.Message to messagesHasNewItemsFlow,
|
||||
Route.Notification to notificationHasNewItemsFlow,
|
||||
Route.Notification() to notificationHasNewItemsFlow,
|
||||
)
|
||||
|
||||
fun isWriteable(): Boolean = account.isWriteable()
|
||||
|
||||
+34
@@ -104,3 +104,37 @@ class MessageSetCard(
|
||||
|
||||
override fun id() = note.idHex
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this card contains a specific event ID.
|
||||
* Used for scrolling to a notification from a push notification intent.
|
||||
*/
|
||||
fun Card.containsEventId(eventId: String): Boolean =
|
||||
when (this) {
|
||||
is NoteCard -> {
|
||||
note.idHex == eventId
|
||||
}
|
||||
|
||||
is BadgeCard -> {
|
||||
note.idHex == eventId
|
||||
}
|
||||
|
||||
is MessageSetCard -> {
|
||||
note.idHex == eventId
|
||||
}
|
||||
|
||||
is ZapUserSetCard -> {
|
||||
zapEvents.any { it.response.idHex == eventId || it.request.idHex == eventId }
|
||||
}
|
||||
|
||||
is MultiSetCard -> {
|
||||
note.idHex == eventId ||
|
||||
zapEvents.any { it.response.idHex == eventId || it.request.idHex == eventId } ||
|
||||
likeEvents.any { it.idHex == eventId } ||
|
||||
boostEvents.any { it.idHex == eventId }
|
||||
}
|
||||
|
||||
else -> {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
+36
-1
@@ -20,8 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -38,8 +40,12 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -66,6 +72,7 @@ import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
fun RenderCardFeed(
|
||||
@@ -75,6 +82,7 @@ fun RenderCardFeed(
|
||||
listState: LazyListState,
|
||||
nav: INav,
|
||||
routeForLastRead: String,
|
||||
scrollToEventId: String? = null,
|
||||
) {
|
||||
val feedState by feedContent.feedContent.collectAsStateWithLifecycle()
|
||||
|
||||
@@ -101,6 +109,7 @@ fun RenderCardFeed(
|
||||
routeForLastRead = routeForLastRead,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
scrollToEventId = scrollToEventId,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -133,10 +142,29 @@ private fun FeedLoaded(
|
||||
routeForLastRead: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
scrollToEventId: String? = null,
|
||||
) {
|
||||
val items by loaded.feed.collectAsStateWithLifecycle()
|
||||
val openPolls by polls.flow.collectAsStateWithLifecycle()
|
||||
|
||||
// Track which card is highlighted (will auto-clear after animation)
|
||||
var highlightedCardId by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
// Scroll to the card containing the target event ID
|
||||
if (scrollToEventId != null) {
|
||||
LaunchedEffect(scrollToEventId, items) {
|
||||
val position = items.list.indexOfFirst { it.containsEventId(scrollToEventId) }
|
||||
if (position >= 0) {
|
||||
// +1 offset for the donation card header item
|
||||
val scrollIndex = position + 1 + openPolls.size
|
||||
listState.animateScrollToItem(scrollIndex)
|
||||
highlightedCardId = items.list[position].id()
|
||||
delay(2000)
|
||||
highlightedCardId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = FeedPadding,
|
||||
@@ -182,7 +210,14 @@ private fun FeedLoaded(
|
||||
key = { _, item -> item.id() },
|
||||
contentType = { _, item -> item.javaClass.simpleName },
|
||||
) { _, item ->
|
||||
Row(Modifier.fillMaxWidth().animateItem()) {
|
||||
val isHighlighted = highlightedCardId == item.id()
|
||||
val highlightColor by animateColorAsState(
|
||||
targetValue = if (isHighlighted) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) else Color.Transparent,
|
||||
animationSpec = tween(durationMillis = if (isHighlighted) 300 else 1000),
|
||||
label = "highlightAnimation",
|
||||
)
|
||||
|
||||
Row(Modifier.fillMaxWidth().background(highlightColor).animateItem()) {
|
||||
logTime(
|
||||
debugMessage = { "CardFeedView $item" },
|
||||
) {
|
||||
|
||||
+6
-3
@@ -42,6 +42,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
fun NotificationScreen(
|
||||
scrollToEventId: String? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@@ -50,6 +51,7 @@ fun NotificationScreen(
|
||||
notifSummaryState = accountViewModel.feedStates.notificationSummary,
|
||||
notifPolls = accountViewModel.feedStates.notificationsOpenPolls,
|
||||
sharedPrefs = accountViewModel.settings.uiSettingsFlow,
|
||||
scrollToEventId = scrollToEventId,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
@@ -61,6 +63,7 @@ fun NotificationScreen(
|
||||
notifSummaryState: NotificationSummaryState,
|
||||
notifPolls: OpenPollsState,
|
||||
sharedPrefs: UiSettingsFlow,
|
||||
scrollToEventId: String? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@@ -79,8 +82,8 @@ fun NotificationScreen(
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
AppBottomBar(Route.Notification, accountViewModel) { route ->
|
||||
if (route == Route.Notification) {
|
||||
AppBottomBar(Route.Notification(), accountViewModel) { route ->
|
||||
if (route is Route.Notification) {
|
||||
notifFeedContentState.invalidateDataAndSendToTop(true)
|
||||
} else {
|
||||
nav.newStack(route)
|
||||
@@ -98,7 +101,7 @@ fun NotificationScreen(
|
||||
|
||||
WatchScrollToTop(notifFeedContentState, listState)
|
||||
|
||||
RenderCardFeed(notifFeedContentState, notifPolls, accountViewModel, listState, nav, "Notification")
|
||||
RenderCardFeed(notifFeedContentState, notifPolls, accountViewModel, listState, nav, "Notification", scrollToEventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user