A cheap attempt at a native Bluesky client for Android
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

SearchView: Replace filter chips with PrimaryTabRow, add from: dialog

Switch search to Latest/Top/People tabs via PrimaryTabRow. Add a
from:user filter chip that opens an AlertDialog to enter a handle,
applying the from: query prefix to post searches.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

geesawra 489b7e52 418a882c

+99 -37
+99 -37
app/src/main/java/industries/geesawra/monarch/SearchView.kt
··· 22 22 import androidx.compose.material.icons.filled.Clear 23 23 import androidx.compose.material.icons.filled.Search 24 24 import androidx.compose.material.icons.filled.Verified 25 + import androidx.compose.material3.AlertDialog 25 26 import androidx.compose.material3.Card 26 27 import androidx.compose.material3.CircularProgressIndicator 27 28 import androidx.compose.material3.ExperimentalMaterial3Api ··· 29 30 import androidx.compose.material3.Icon 30 31 import androidx.compose.material3.IconButton 31 32 import androidx.compose.material3.MaterialTheme 33 + import androidx.compose.material3.OutlinedTextField 34 + import androidx.compose.material3.PrimaryTabRow 32 35 import androidx.compose.material3.SearchBar 33 36 import androidx.compose.material3.SearchBarDefaults 37 + import androidx.compose.material3.Tab 34 38 import androidx.compose.material3.Text 39 + import androidx.compose.material3.TextButton 35 40 import androidx.compose.runtime.Composable 36 41 import androidx.compose.runtime.LaunchedEffect 37 42 import androidx.compose.runtime.derivedStateOf ··· 73 78 onProfileTap: (Did) -> Unit, 74 79 ) { 75 80 var query by rememberSaveable { mutableStateOf("") } 76 - var showPeople by rememberSaveable { mutableStateOf(false) } 81 + var selectedTab by rememberSaveable { mutableStateOf(0) } 77 82 78 83 Column( 79 84 modifier = modifier ··· 112 117 .padding(horizontal = 16.dp), 113 118 ) {} 114 119 115 - // Filter chips row 116 - SearchFilters(viewModel, showPeople, onShowPeopleChange = { showPeople = it }) 120 + // from: author filter row 121 + AuthorFilterRow(viewModel) 122 + 123 + // Tabs 124 + SearchTabs( 125 + selectedTab = selectedTab, 126 + onTabSelected = { index -> 127 + selectedTab = index 128 + when (index) { 129 + 0 -> viewModel.setSearchSort(SearchPostsSort.Latest) 130 + 1 -> viewModel.setSearchSort(SearchPostsSort.Top) 131 + } 132 + }, 133 + ) 117 134 118 135 // Content 119 - if (showPeople) { 120 - SearchPeopleResults( 136 + when (selectedTab) { 137 + 0, 1 -> SearchPostsResults( 121 138 viewModel = viewModel, 122 - listState = peopleListState, 139 + listState = postsListState, 123 140 isScrollEnabled = isScrollEnabled, 141 + onThreadTap = onThreadTap, 124 142 onProfileTap = onProfileTap, 125 143 ) 126 - } else { 127 - SearchPostsResults( 144 + 145 + 2 -> SearchPeopleResults( 128 146 viewModel = viewModel, 129 - listState = postsListState, 147 + listState = peopleListState, 130 148 isScrollEnabled = isScrollEnabled, 131 - onThreadTap = onThreadTap, 132 149 onProfileTap = onProfileTap, 133 150 ) 134 151 } 135 152 } 136 153 } 137 154 155 + private val searchTabs = listOf("Latest", "Top", "People") 156 + 157 + @OptIn(ExperimentalMaterial3Api::class) 138 158 @Composable 139 - private fun SearchFilters( 140 - viewModel: TimelineViewModel, 141 - showPeople: Boolean, 142 - onShowPeopleChange: (Boolean) -> Unit, 159 + private fun SearchTabs( 160 + selectedTab: Int, 161 + onTabSelected: (Int) -> Unit, 143 162 ) { 144 - val sort = viewModel.uiState.searchPostsSort 163 + PrimaryTabRow(selectedTabIndex = selectedTab) { 164 + searchTabs.forEachIndexed { index, label -> 165 + Tab( 166 + selected = selectedTab == index, 167 + onClick = { onTabSelected(index) }, 168 + text = { Text(label) }, 169 + ) 170 + } 171 + } 172 + } 173 + 174 + @Composable 175 + private fun AuthorFilterRow(viewModel: TimelineViewModel) { 145 176 val authorFilter = viewModel.uiState.searchAuthorFilter 177 + var showDialog by remember { mutableStateOf(false) } 178 + 179 + if (showDialog) { 180 + FromAuthorDialog( 181 + onDismiss = { showDialog = false }, 182 + onConfirm = { handle -> 183 + viewModel.setSearchAuthorFilter(handle) 184 + showDialog = false 185 + }, 186 + ) 187 + } 146 188 147 189 Row( 148 190 modifier = Modifier 149 191 .fillMaxWidth() 150 - .padding(horizontal = 16.dp, vertical = 8.dp), 192 + .padding(horizontal = 16.dp, vertical = 4.dp), 151 193 horizontalArrangement = Arrangement.spacedBy(8.dp), 152 194 ) { 153 - FilterChip( 154 - selected = !showPeople && sort == SearchPostsSort.Latest, 155 - onClick = { 156 - onShowPeopleChange(false) 157 - viewModel.setSearchSort(SearchPostsSort.Latest) 158 - }, 159 - label = { Text("Latest") }, 160 - ) 161 - FilterChip( 162 - selected = !showPeople && sort == SearchPostsSort.Top, 163 - onClick = { 164 - onShowPeopleChange(false) 165 - viewModel.setSearchSort(SearchPostsSort.Top) 166 - }, 167 - label = { Text("Top") }, 168 - ) 169 - FilterChip( 170 - selected = showPeople, 171 - onClick = { onShowPeopleChange(!showPeople) }, 172 - label = { Text("People") }, 173 - ) 174 195 if (authorFilter != null) { 175 196 FilterChip( 176 197 selected = true, ··· 184 205 ) 185 206 }, 186 207 ) 208 + } else { 209 + FilterChip( 210 + selected = false, 211 + onClick = { showDialog = true }, 212 + label = { Text("from:user") }, 213 + ) 187 214 } 188 215 } 216 + } 217 + 218 + @Composable 219 + private fun FromAuthorDialog( 220 + onDismiss: () -> Unit, 221 + onConfirm: (String) -> Unit, 222 + ) { 223 + var handle by remember { mutableStateOf("") } 224 + 225 + AlertDialog( 226 + onDismissRequest = onDismiss, 227 + title = { Text("Filter by author") }, 228 + text = { 229 + OutlinedTextField( 230 + value = handle, 231 + onValueChange = { handle = it }, 232 + label = { Text("Handle") }, 233 + placeholder = { Text("e.g. alice.bsky.social") }, 234 + singleLine = true, 235 + modifier = Modifier.fillMaxWidth(), 236 + ) 237 + }, 238 + confirmButton = { 239 + TextButton( 240 + onClick = { if (handle.isNotBlank()) onConfirm(handle.trim()) }, 241 + ) { 242 + Text("Apply") 243 + } 244 + }, 245 + dismissButton = { 246 + TextButton(onClick = onDismiss) { 247 + Text("Cancel") 248 + } 249 + }, 250 + ) 189 251 } 190 252 191 253 @Composable