Merge pull request #1961 from vitorpamplona/claude/pinned-notes-in-tab-8y3TR

Integrate pinned notes into new threads tab
This commit is contained in:
Vitor Pamplona
2026-03-27 10:25:26 -04:00
committed by GitHub
2 changed files with 187 additions and 18 deletions
@@ -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) },
@@ -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),
)
}
}