diff --git a/app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt b/app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt index d46edb6..6d414b6 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.* import kotlinx.coroutines.launch import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager @@ -83,6 +84,7 @@ import com.swoosh.microblog.data.CredentialsManager import com.swoosh.microblog.data.ShareUtils import com.swoosh.microblog.data.model.FeedPost import com.swoosh.microblog.data.model.GhostAccount +import com.swoosh.microblog.data.model.GhostTagFull import com.swoosh.microblog.data.model.PostFilter import com.swoosh.microblog.data.model.PostStats import com.swoosh.microblog.data.model.QueueStatus @@ -109,6 +111,7 @@ fun FeedScreen( val recentSearches by viewModel.recentSearches.collectAsStateWithLifecycle() val accounts by viewModel.accounts.collectAsStateWithLifecycle() val activeAccount by viewModel.activeAccount.collectAsStateWithLifecycle() + val popularTags by viewModel.popularTags.collectAsStateWithLifecycle() val listState = rememberLazyListState() val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } @@ -305,20 +308,17 @@ fun FeedScreen( ) } - // Active tag filter bar - if (state.activeTagFilter != null) { - FilterChip( - onClick = { viewModel.clearTagFilter() }, - label = { Text("#${state.activeTagFilter}") }, - selected = true, - leadingIcon = { - Icon(Icons.Default.Tag, contentDescription = null, modifier = Modifier.size(16.dp)) - }, - trailingIcon = { - Icon(Icons.Default.Close, contentDescription = "Clear filter", modifier = Modifier.size(16.dp)) - }, - modifier = Modifier - .padding(horizontal = 16.dp, vertical = 4.dp) + // Tag filter chips + AnimatedVisibility( + visible = !isSearchActive && popularTags.isNotEmpty(), + enter = fadeIn(SwooshMotion.quick()) + expandVertically(), + exit = fadeOut(SwooshMotion.quick()) + shrinkVertically() + ) { + TagFilterChipsBar( + tags = popularTags, + activeTagFilter = state.activeTagFilter, + onTagSelected = { viewModel.filterByTag(it) }, + onClearFilter = { viewModel.clearTagFilter() } ) } @@ -775,6 +775,58 @@ fun FilterChipsBar( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TagFilterChipsBar( + tags: List, + activeTagFilter: String?, + onTagSelected: (String) -> Unit, + onClearFilter: () -> Unit +) { + LazyRow( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + // "All tags" chip + item { + val isAllSelected = activeTagFilter == null + FilterChip( + selected = isAllSelected, + onClick = { onClearFilter() }, + label = { Text("All tags") }, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = MaterialTheme.colorScheme.primaryContainer + ) + ) + } + + // Tag chips + items(tags, key = { it.id ?: it.name }) { tag -> + val isSelected = activeTagFilter != null && activeTagFilter.equals(tag.name, ignoreCase = true) + FilterChip( + selected = isSelected, + onClick = { + if (isSelected) onClearFilter() else onTagSelected(tag.name) + }, + label = { + val postCount = tag.count?.posts + if (postCount != null) { + Text("${tag.name} ($postCount)") + } else { + Text(tag.name) + } + }, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = MaterialTheme.colorScheme.primaryContainer + ) + ) + } + } +} + @Composable fun SortButton( currentSort: SortOrder, @@ -1519,23 +1571,19 @@ fun PostCardContent( } } - // Hashtag tags (bold colored text, not chips) + // Tags display if (post.tags.isNotEmpty()) { Spacer(modifier = Modifier.height(10.dp)) - @OptIn(ExperimentalLayoutApi::class) - FlowRow( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - post.tags.forEach { tag -> - Text( - text = "#$tag", - style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Bold), - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.clickable { onTagClick(tag) } - ) + Text( + text = post.tags.joinToString(" \u00B7 ") { "#$it" }, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.clickable { + post.tags.firstOrNull()?.let { onTagClick(it) } } - } + ) } // Queue status diff --git a/app/src/main/java/com/swoosh/microblog/ui/feed/FeedViewModel.kt b/app/src/main/java/com/swoosh/microblog/ui/feed/FeedViewModel.kt index 92d3e63..744a3cb 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/feed/FeedViewModel.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/feed/FeedViewModel.kt @@ -13,6 +13,7 @@ import com.swoosh.microblog.data.api.ApiClient import com.swoosh.microblog.data.db.Converters import com.swoosh.microblog.data.model.* import com.swoosh.microblog.data.repository.PostRepository +import com.swoosh.microblog.data.repository.TagRepository import com.google.gson.Gson import com.google.gson.reflect.TypeToken import kotlinx.coroutines.FlowPreview @@ -38,6 +39,7 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) { private val accountManager = AccountManager(application) private var repository = PostRepository(application) + private var tagRepository = TagRepository(application) private val feedPreferences = FeedPreferences(application) private val searchHistoryManager = SearchHistoryManager(application) @@ -71,6 +73,9 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) { private val _recentSearches = MutableStateFlow>(emptyList()) val recentSearches: StateFlow> = _recentSearches.asStateFlow() + private val _popularTags = MutableStateFlow>(emptyList()) + val popularTags: StateFlow> = _popularTags.asStateFlow() + private val _accounts = MutableStateFlow>(emptyList()) val accounts: StateFlow> = _accounts.asStateFlow() @@ -237,8 +242,9 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) { accountManager.setActiveAccount(accountId) ApiClient.reset() - // Re-create repository to pick up new account + // Re-create repositories to pick up new account repository = PostRepository(getApplication()) + tagRepository = TagRepository(getApplication()) refreshAccountsList() @@ -296,6 +302,18 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) { val sort = _sortOrder.value val tagFilter = _uiState.value.activeTagFilter + // Fetch popular tags in parallel + launch { + tagRepository.fetchTags().fold( + onSuccess = { tags -> + _popularTags.value = tags + .sortedByDescending { it.count?.posts ?: 0 } + .take(10) + }, + onFailure = { /* silently ignore tag fetch failures */ } + ) + } + repository.fetchPosts(page = 1, filter = filter, sortOrder = sort, tagFilter = tagFilter).fold( onSuccess = { response -> remotePosts = response.posts.map { it.toFeedPost() }