From 7618a5039d32186816b6b92217009e49bfe5e581 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Mar 2026 14:12:06 +0000 Subject: [PATCH] feat: show pinned notes at top of Notes tab instead of separate tab Remove the dedicated Pinned Notes tab from the profile and display pinned notes at the beginning of the Notes tab with a pin icon marker. https://claude.ai/code/session_01ChamaudsEwwQ8mNtqnx4ze --- .../screen/loggedIn/profile/ProfileScreen.kt | 16 +- .../profile/newthreads/TabNotesNewThreads.kt | 189 +++++++++++++++++- 2 files changed, 187 insertions(+), 18 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt index 90565778b..2f6e0c037 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt @@ -87,8 +87,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.TabMutualCon import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.dal.UserProfileMutualFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.TabNotesNewThreads import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.dal.UserProfileNewThreadsFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.pinnedNotes.PinnedNotesTabHeader -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.pinnedNotes.TabPinnedNotes import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.pinnedNotes.dal.UserProfilePinnedNotesFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays.RelaysTabHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays.TabRelays @@ -408,7 +406,7 @@ private fun RenderScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val pagerState = rememberPagerState { 12 } + val pagerState = rememberPagerState { 11 } Column { ProfileHeader(baseUser, appRecommendations, externalIdentities, nav, accountViewModel) @@ -430,7 +428,6 @@ private fun RenderScreen( followersFeedViewModel, zapFeedViewModel, bookmarksFeedViewModel, - pinnedNotesFeedViewModel, galleryFeedViewModel, reportsFeedViewModel, accountViewModel, @@ -485,7 +482,7 @@ private fun CreateAndRenderPages( ) when (page) { - 0 -> TabNotesNewThreads(threadsViewModel, accountViewModel, nav) + 0 -> TabNotesNewThreads(threadsViewModel, pinnedNotesFeedViewModel, accountViewModel, nav) 1 -> TabNotesConversations(repliesViewModel, accountViewModel, nav) 2 -> TabMutualConversations(mutualViewModel, accountViewModel, nav) 3 -> TabGallery(galleryFeedViewModel, accountViewModel, nav) @@ -493,10 +490,9 @@ private fun CreateAndRenderPages( 5 -> TabFollowers(followersFeedViewModel, accountViewModel, nav) 6 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav) 7 -> TabBookmarks(bookmarksFeedViewModel, accountViewModel, nav) - 8 -> TabPinnedNotes(pinnedNotesFeedViewModel, accountViewModel, nav) - 9 -> TabFollowedTags(baseUser, accountViewModel, nav) - 10 -> TabReports(baseUser, reportsFeedViewModel, accountViewModel, nav) - 11 -> TabRelays(baseUser, accountViewModel, nav) + 8 -> TabFollowedTags(baseUser, accountViewModel, nav) + 9 -> TabReports(baseUser, reportsFeedViewModel, accountViewModel, nav) + 10 -> TabRelays(baseUser, accountViewModel, nav) } } @@ -526,7 +522,6 @@ private fun CreateAndRenderTabs( followersFeedViewModel: UserProfileFollowersUserFeedViewModel, zapFeedViewModel: UserProfileZapsViewModel, bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel, - pinnedNotesFeedViewModel: UserProfilePinnedNotesFeedViewModel, galleryFeedViewModel: UserProfileGalleryFeedViewModel, reportsFeedViewModel: UserProfileReportFeedViewModel, accountViewModel: AccountViewModel, @@ -543,7 +538,6 @@ private fun CreateAndRenderTabs( { FollowersTabHeader(baseUser, followersFeedViewModel, accountViewModel) }, { ZapTabHeader(zapFeedViewModel, accountViewModel) }, { BookmarkTabHeader(baseUser, accountViewModel) }, - { PinnedNotesTabHeader(baseUser, accountViewModel) }, { FollowedTagsTabHeader(baseUser, accountViewModel) }, { ReportsTabHeader(baseUser, reportsFeedViewModel, accountViewModel) }, { RelaysTabHeader(baseUser, accountViewModel) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/TabNotesNewThreads.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/TabNotesNewThreads.kt index c2d523570..85d03e3ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/TabNotesNewThreads.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/TabNotesNewThreads.kt @@ -20,28 +20,203 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PushPin +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState +import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty +import com.vitorpamplona.amethyst.ui.feeds.FeedError +import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed +import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox +import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop import com.vitorpamplona.amethyst.ui.navigation.navs.INav -import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView +import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.dal.UserProfileNewThreadsFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.pinnedNotes.dal.UserProfilePinnedNotesFeedViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.FeedPadding @Composable fun TabNotesNewThreads( feedViewModel: UserProfileNewThreadsFeedViewModel, + pinnedNotesFeedViewModel: UserProfilePinnedNotesFeedViewModel, accountViewModel: AccountViewModel, nav: INav, ) { + LaunchedEffect(Unit) { pinnedNotesFeedViewModel.invalidateData() } + Column(Modifier.fillMaxHeight()) { - RefresheableFeedView( - feedViewModel, - null, - enablePullRefresh = false, - accountViewModel = accountViewModel, - nav = nav, + RefresheableBox(feedViewModel, enablePullRefresh = false) { + val listState = + androidx.compose.foundation.lazy + .rememberLazyListState() + + WatchScrollToTop(feedViewModel.feedState, listState) + + val feedState by feedViewModel.feedState.feedContent.collectAsStateWithLifecycle() + val pinnedFeedState by pinnedNotesFeedViewModel.feedState.feedContent.collectAsStateWithLifecycle() + + when (val state = feedState) { + is FeedState.Empty -> { + val pinnedLoaded = pinnedFeedState as? FeedState.Loaded + if (pinnedLoaded != null) { + val pinnedItems by pinnedLoaded.feed.collectAsStateWithLifecycle() + if (pinnedItems.list.isNotEmpty()) { + FeedLoadedWithPinnedNotes( + pinnedFeedState = pinnedLoaded, + loaded = null, + listState = listState, + accountViewModel = accountViewModel, + nav = nav, + ) + } else { + FeedEmpty { feedViewModel.invalidateData() } + } + } else { + FeedEmpty { feedViewModel.invalidateData() } + } + } + + is FeedState.FeedError -> { + FeedError(state.errorMessage) { feedViewModel.invalidateData() } + } + + is FeedState.Loaded -> { + FeedLoadedWithPinnedNotes( + pinnedFeedState = pinnedFeedState as? FeedState.Loaded, + loaded = state, + listState = listState, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + is FeedState.Loading -> { + LoadingFeed() + } + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun FeedLoadedWithPinnedNotes( + pinnedFeedState: FeedState.Loaded?, + loaded: FeedState.Loaded?, + listState: LazyListState, + accountViewModel: AccountViewModel, + nav: INav, +) { + val pinnedItems = + pinnedFeedState?.let { + val state by it.feed.collectAsStateWithLifecycle() + state + } + + val feedItems = + loaded?.let { + val state by it.feed.collectAsStateWithLifecycle() + state + } + + LazyColumn( + contentPadding = FeedPadding, + state = listState, + ) { + if (pinnedItems != null && pinnedItems.list.isNotEmpty()) { + items( + pinnedItems.list, + key = { "pinned-${it.idHex}" }, + contentType = { it.event?.kind ?: -1 }, + ) { item -> + Column(Modifier.fillMaxWidth().animateItem()) { + PinnedNoteLabel() + NoteCompose( + item, + modifier = Modifier.fillMaxWidth(), + routeForLastRead = null, + isBoostedNote = false, + isHiddenFeed = pinnedItems.showHidden, + quotesLeft = 3, + accountViewModel = accountViewModel, + nav = nav, + ) + } + HorizontalDivider(thickness = DividerThickness) + } + } + + if (feedItems != null) { + itemsIndexed( + feedItems.list, + key = { _, item -> item.idHex }, + contentType = { _, item -> item.event?.kind ?: -1 }, + ) { _, item -> + Row(Modifier.fillMaxWidth().animateItem()) { + NoteCompose( + item, + modifier = Modifier.fillMaxWidth(), + routeForLastRead = null, + isBoostedNote = false, + isHiddenFeed = feedItems.showHidden, + quotesLeft = 3, + accountViewModel = accountViewModel, + nav = nav, + ) + } + HorizontalDivider(thickness = DividerThickness) + } + } + } +} + +@Composable +private fun PinnedNoteLabel() { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 0.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Default.PushPin, + contentDescription = stringRes(R.string.pinned_notes), + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), + ) + Spacer(Modifier.width(4.dp)) + Text( + text = stringRes(R.string.pinned_notes), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), ) } }