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

Configure Feed

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

*: Redesign timeline and notifications for Material 3 compliance

Use proper M3 tonal layering with surface/surfaceContainer colors,
ElevatedCards, SuggestionChips, left-aligned action buttons, inline
timestamps, and compact FlowRow notification avatars.

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

geesawra 0607a770 f4f94e13

+291 -280
+79
CLAUDE.md
··· 136 136 137 137 For record creation, use `BlueskyJson.encodeAsJsonContent()` to convert typed records to `JsonContent` for `createRecord`/`putRecord`. 138 138 139 + ## Material 3 Design Guidelines 140 + 141 + Reference: https://m3.material.io/ 142 + 143 + ### Color System 144 + 145 + Five key colors, each with a tonal palette of 13 tones: 146 + 147 + | Role | Purpose | 148 + |------|---------| 149 + | **Primary** | Main components, prominent buttons, active states, elevated surface tint | 150 + | **Secondary** | Less prominent components (filter chips), color expression | 151 + | **Tertiary** | Contrasting accents to balance primary/secondary | 152 + | **Surface** | Backgrounds and container surfaces | 153 + | **Error** | Error states and destructive actions | 154 + 155 + Color pairing rules — always use matching on-colors: 156 + - `onPrimary` on `primary`, `onPrimaryContainer` on `primaryContainer` 157 + - `onSurface` for high-emphasis text, `onSurfaceVariant` for medium-emphasis 158 + - Never mix incompatible pairs (e.g. `tertiaryContainer` + `primaryContainer`) 159 + 160 + Dynamic color (Android 12+): use `dynamicLightColorScheme()` / `dynamicDarkColorScheme()` with static fallback. 161 + 162 + ### Typography Scale (15 styles) 163 + 164 + | Category | Large | Medium | Small | 165 + |----------|-------|--------|-------| 166 + | **Display** | 57/64sp | 45/52sp | 36/44sp | 167 + | **Headline** | 32/40sp | 28/36sp | 24/32sp | 168 + | **Title** | 22/28sp | 16/24sp, w500 | 14/20sp, w500 | 169 + | **Body** | 16/24sp | 14/20sp | 12/16sp | 170 + | **Label** | 14/20sp, w500 | 12/16sp, w500 | 11/16sp, w500 | 171 + 172 + Access via `MaterialTheme.typography.titleLarge`, `.bodyMedium`, etc. 173 + 174 + ### Shape Scale 175 + 176 + | Size | Corner Radius | 177 + |------|--------------| 178 + | Extra Small | 4.dp | 179 + | Small | 8.dp | 180 + | Medium | 12.dp | 181 + | Large | 16.dp | 182 + | Extra Large | 24.dp | 183 + 184 + Access via `MaterialTheme.shapes.medium`, etc. Also: `RectangleShape`, `CircleShape`. 185 + 186 + ### Elevation 187 + 188 + M3 uses **tonal color overlays** (surface tint) instead of shadows. Use `tonalElevation` for visual hierarchy, `shadowElevation` sparingly for floating elements. 189 + 190 + ### Button Emphasis Hierarchy (high → low) 191 + 192 + 1. `ExtendedFloatingActionButton` / `FloatingActionButton` — highest emphasis 193 + 2. `Button` (filled) — high emphasis 194 + 3. `FilledTonalButton` — medium-high 195 + 4. `OutlinedButton` — medium 196 + 5. `TextButton` — low emphasis 197 + 198 + ### Navigation by Screen Size 199 + 200 + | Device | Component | 201 + |--------|-----------| 202 + | Compact (phone) | `NavigationBar` (bottom, ≤5 destinations) | 203 + | Medium (tablet landscape) | `NavigationRail` (side) | 204 + | Large (tablet/desktop) | `PermanentNavigationDrawer` | 205 + 206 + ### Component Color Customization 207 + 208 + Use `*Defaults` objects: `CardDefaults.cardColors()`, `ButtonDefaults.buttonColors()`, `CardDefaults.cardElevation()`, etc. 209 + 210 + ### Key Principles 211 + 212 + - Tonal palettes ensure accessible contrast automatically 213 + - Support both light and dark themes 214 + - Use `MaterialTheme.colorScheme.*` for all colors — avoid hardcoded values 215 + - Pair container colors with their on-container counterparts 216 + - Use typography scale roles semantically (display for hero text, body for content, label for buttons) 217 + 139 218 ## Guidelines 140 219 141 220 - Use Material 3 / Material You components and patterns
+139 -178
app/src/main/java/industries/geesawra/monarch/LikeRepostRowView.kt
··· 15 15 import androidx.compose.foundation.layout.Arrangement 16 16 import androidx.compose.foundation.layout.Box 17 17 import androidx.compose.foundation.layout.Column 18 + import androidx.compose.foundation.layout.ExperimentalLayoutApi 19 + import androidx.compose.foundation.layout.FlowRow 18 20 import androidx.compose.foundation.layout.Row 21 + import androidx.compose.foundation.layout.Spacer 19 22 import androidx.compose.foundation.layout.fillMaxWidth 20 23 import androidx.compose.foundation.layout.offset 21 24 import androidx.compose.foundation.layout.padding 22 25 import androidx.compose.foundation.layout.size 26 + import androidx.compose.foundation.layout.width 23 27 import androidx.compose.foundation.layout.wrapContentHeight 24 28 import androidx.compose.foundation.shape.CircleShape 25 29 import androidx.compose.material.icons.Icons 26 30 import androidx.compose.material.icons.filled.KeyboardArrowUp 27 31 import androidx.compose.material.icons.filled.Repeat 32 + import androidx.compose.material3.Card 33 + import androidx.compose.material3.CardDefaults 28 34 import androidx.compose.material3.Icon 29 35 import androidx.compose.material3.IconButton 30 36 import androidx.compose.material3.MaterialTheme 31 - import androidx.compose.material3.OutlinedCard 32 - import androidx.compose.material3.Surface 33 37 import androidx.compose.material3.Text 34 38 import androidx.compose.runtime.Composable 35 39 import androidx.compose.runtime.mutableStateOf ··· 37 41 import androidx.compose.ui.Alignment 38 42 import androidx.compose.ui.Modifier 39 43 import androidx.compose.ui.draw.clip 40 - import androidx.compose.ui.graphics.Color 41 44 import androidx.compose.ui.graphics.ColorFilter 42 45 import androidx.compose.ui.platform.LocalContext 43 46 import androidx.compose.ui.text.font.FontWeight ··· 65 68 } 66 69 } 67 70 68 - @OptIn(ExperimentalTime::class) 71 + @OptIn(ExperimentalTime::class, ExperimentalLayoutApi::class) 69 72 @Composable 70 73 fun LikeRepostRowView( 71 74 modifier: Modifier = Modifier, 72 75 data: RepeatedNotification, 73 76 onShowThread: (SkeetData) -> Unit = {}, 74 77 ) { 75 - val minSize = 24.dp 78 + val avatarSize = 28.dp 76 79 val showAvatars = remember { mutableStateOf(false) } 77 80 78 - Surface( 79 - color = Color.Transparent, 81 + Column( 80 82 modifier = modifier 81 83 .padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 8.dp) 82 84 .fillMaxWidth() ··· 87 89 val firstAuthor = authors.first() 88 90 val firstAuthorName = name(firstAuthor.author) 89 91 val remainingCount = authors.size - 1 92 + val actionVerb = when (data.kind) { 93 + RepeatableNotification.Like -> "liked" 94 + RepeatableNotification.Repost -> "reposted" 95 + } 90 96 val reasonLine = when { 91 - remainingCount > 1 -> "$firstAuthorName and $remainingCount others ${ 92 - when (data.kind) { 93 - RepeatableNotification.Like -> "liked" 94 - RepeatableNotification.Repost -> "reposted" 95 - } 96 - } this" 97 - 98 - remainingCount == 1 -> "$firstAuthorName and 1 other ${ 99 - when (data.kind) { 100 - RepeatableNotification.Like -> "liked" 101 - RepeatableNotification.Repost -> "reposted" 102 - } 103 - } this" 97 + remainingCount > 1 -> "$firstAuthorName and $remainingCount others $actionVerb this" 98 + remainingCount == 1 -> "$firstAuthorName and 1 other $actionVerb this" 99 + else -> "$firstAuthorName $actionVerb this" 100 + } 104 101 105 - else -> "$firstAuthorName ${ 106 - when (data.kind) { 107 - RepeatableNotification.Like -> "liked" 108 - RepeatableNotification.Repost -> "reposted" 109 - } 110 - } this" 102 + Row( 103 + verticalAlignment = Alignment.CenterVertically, 104 + modifier = Modifier 105 + .fillMaxWidth() 106 + .padding(bottom = 4.dp) 107 + ) { 108 + Image( 109 + imageVector = when (data.kind) { 110 + RepeatableNotification.Like -> HeartFilled 111 + RepeatableNotification.Repost -> Icons.Default.Repeat 112 + }, 113 + colorFilter = ColorFilter.tint( 114 + when (data.kind) { 115 + RepeatableNotification.Like -> MaterialTheme.colorScheme.error 116 + RepeatableNotification.Repost -> MaterialTheme.colorScheme.onSurfaceVariant 117 + } 118 + ), 119 + contentDescription = "${ 120 + when (data.kind) { 121 + RepeatableNotification.Like -> "Like" 122 + RepeatableNotification.Repost -> "Repost" 123 + } 124 + } icon", 125 + modifier = Modifier.size(20.dp) 126 + ) 127 + Spacer(modifier = Modifier.width(8.dp)) 128 + Text( 129 + text = reasonLine, 130 + style = MaterialTheme.typography.bodyMedium, 131 + fontWeight = FontWeight.Bold, 132 + modifier = Modifier.weight(1f) 133 + ) 134 + Text( 135 + text = HumanReadable.timeAgo(data.timestamp), 136 + color = MaterialTheme.colorScheme.onSurfaceVariant, 137 + style = MaterialTheme.typography.labelSmall, 138 + ) 111 139 } 112 140 113 - Column( 114 - verticalArrangement = Arrangement.spacedBy(4.dp) 115 - ) { 116 - AnimatedContent( 117 - targetState = showAvatars.value, 118 - transitionSpec = { 119 - fadeIn(animationSpec = tween(150, 150)) togetherWith 120 - fadeOut(animationSpec = tween(150)) using 121 - SizeTransform { initialSize, targetSize -> 122 - if (targetState) { 123 - keyframes { 124 - IntSize(targetSize.width, initialSize.height) at 150 125 - durationMillis = 300 126 - } 127 - } else { 128 - keyframes { 129 - IntSize(initialSize.width, targetSize.height) at 150 130 - durationMillis = 300 131 - } 141 + AnimatedContent( 142 + targetState = showAvatars.value, 143 + transitionSpec = { 144 + fadeIn(animationSpec = tween(150, 150)) togetherWith 145 + fadeOut(animationSpec = tween(150)) using 146 + SizeTransform { initialSize, targetSize -> 147 + if (targetState) { 148 + keyframes { 149 + IntSize(targetSize.width, initialSize.height) at 150 150 + durationMillis = 300 151 + } 152 + } else { 153 + keyframes { 154 + IntSize(initialSize.width, targetSize.height) at 150 155 + durationMillis = 300 132 156 } 133 157 } 134 - }, label = "size transform" 135 - ) { 136 - when (it) { 137 - true -> Column( 138 - verticalArrangement = Arrangement.spacedBy(4.dp) 158 + } 159 + }, label = "size transform" 160 + ) { 161 + when (it) { 162 + true -> Column { 163 + FlowRow( 164 + horizontalArrangement = Arrangement.spacedBy(8.dp), 165 + verticalArrangement = Arrangement.spacedBy(8.dp), 166 + modifier = Modifier 167 + .fillMaxWidth() 168 + .padding(bottom = 8.dp) 139 169 ) { 140 - IconButton( 141 - modifier = Modifier.align(Alignment.End), 142 - onClick = { 143 - showAvatars.value = !showAvatars.value 144 - } 145 - ) { 146 - Icon( 147 - imageVector = Icons.Default.KeyboardArrowUp, 148 - contentDescription = "Close avatar list", 149 - ) 150 - } 151 - data.authors.take(8).forEachIndexed { idx, it -> 170 + data.authors.take(8).forEach { 152 171 Row( 153 172 verticalAlignment = Alignment.CenterVertically, 154 - horizontalArrangement = Arrangement.Start, 155 173 modifier = Modifier 156 - .fillMaxWidth() 157 174 .clickable { 158 175 Log.d( 159 176 "LikeRepostRowView", ··· 169 186 placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 170 187 contentDescription = "Avatar", 171 188 modifier = Modifier 172 - .size( 173 - when (data.kind) { 174 - RepeatableNotification.Like -> minSize + 8.dp 175 - RepeatableNotification.Repost -> minSize 176 - } 177 - ) 189 + .size(avatarSize) 178 190 .border( 179 191 width = 1.dp, 180 - color = MaterialTheme.colorScheme.surface, 192 + color = MaterialTheme.colorScheme.surfaceContainerLow, 181 193 shape = CircleShape 182 194 ) 183 195 .clip(CircleShape) 184 196 ) 185 - 197 + Spacer(modifier = Modifier.width(4.dp)) 186 198 Text( 187 - modifier = Modifier 188 - .fillMaxWidth() 189 - .padding(start = 4.dp), 190 199 text = name(it.author), 191 - style = MaterialTheme.typography.bodyMedium, 200 + style = MaterialTheme.typography.labelMedium, 192 201 fontWeight = FontWeight.Bold, 193 202 ) 194 203 } 195 204 } 196 205 } 197 - 198 - false -> Column { 199 - Box( 200 - modifier = Modifier 201 - .clickable { 202 - if (data.authors.count() > 1) { 203 - showAvatars.value = !showAvatars.value 204 - } 205 - } 206 - .fillMaxWidth() 207 - ) { 208 - data.authors.take(8).reversed().forEachIndexed { idx, it -> 209 - AsyncImage( 210 - model = ImageRequest.Builder(LocalContext.current) 211 - .data(it.author.avatar?.uri) 212 - .crossfade(true) 213 - .build(), 214 - placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 215 - contentDescription = "Avatar", 216 - modifier = Modifier 217 - .size( 218 - when (data.kind) { 219 - RepeatableNotification.Like -> minSize + 8.dp 220 - RepeatableNotification.Repost -> minSize 221 - } 222 - ) 223 - .offset( 224 - x = when (idx) { 225 - 0 -> 0.dp 226 - else -> (idx * 16).dp 227 - } 228 - ) 229 - .border( 230 - width = 1.dp, 231 - color = MaterialTheme.colorScheme.surface, 232 - shape = CircleShape 233 - ) 234 - .clip(CircleShape) 235 - ) 236 - } 206 + IconButton( 207 + modifier = Modifier.align(Alignment.End), 208 + onClick = { 209 + showAvatars.value = !showAvatars.value 237 210 } 238 - 239 - Text( 240 - modifier = Modifier.fillMaxWidth(), 241 - text = reasonLine, 242 - style = MaterialTheme.typography.bodyMedium, 243 - fontWeight = FontWeight.Bold, 211 + ) { 212 + Icon( 213 + imageVector = Icons.Default.KeyboardArrowUp, 214 + contentDescription = "Close avatar list", 244 215 ) 245 - 246 216 } 247 217 } 248 - } 249 218 250 - Row( 251 - verticalAlignment = Alignment.Top, 252 - horizontalArrangement = Arrangement.Start, 253 - modifier = Modifier 254 - .fillMaxWidth() 255 - .wrapContentHeight() 256 - ) { 257 - Image( 258 - imageVector = when (data.kind) { 259 - RepeatableNotification.Like -> { 260 - HeartFilled 261 - } 262 - 263 - RepeatableNotification.Repost -> { 264 - Icons.Default.Repeat 265 - } 266 - }, 267 - colorFilter = ColorFilter.tint( 268 - when (data.kind) { 269 - RepeatableNotification.Like -> MaterialTheme.colorScheme.error 270 - RepeatableNotification.Repost -> MaterialTheme.colorScheme.inverseSurface 271 - } 272 - ), 273 - contentDescription = "${ 274 - when (data.kind) { 275 - RepeatableNotification.Like -> "Like" 276 - RepeatableNotification.Repost -> "Repost" 277 - } 278 - } icon", 279 - modifier = Modifier 280 - .size(minSize) 281 - ) 282 - 283 - Column( 219 + false -> Box( 284 220 modifier = Modifier 285 - .weight(1f) 286 - .padding(start = 16.dp) 221 + .clickable { 222 + if (data.authors.count() > 1) { 223 + showAvatars.value = !showAvatars.value 224 + } 225 + } 226 + .fillMaxWidth() 227 + .padding(bottom = 4.dp) 287 228 ) { 288 - Text( 289 - text = HumanReadable.timeAgo(data.timestamp), 290 - color = MaterialTheme.colorScheme.onSurfaceVariant, 291 - style = MaterialTheme.typography.bodySmall, 292 - textAlign = TextAlign.End, 293 - modifier = Modifier.fillMaxWidth() 294 - ) 295 - 296 - OutlinedCard( 297 - modifier = Modifier.padding(top = 8.dp) 298 - ) { 299 - SkeetView( 300 - modifier = Modifier.padding(bottom = 8.dp), 301 - viewModel = null, 302 - skeet = data.post, 303 - nested = true, 304 - showLabels = false, 305 - onShowThread = onShowThread, 229 + data.authors.take(8).reversed().forEachIndexed { idx, it -> 230 + AsyncImage( 231 + model = ImageRequest.Builder(LocalContext.current) 232 + .data(it.author.avatar?.uri) 233 + .crossfade(true) 234 + .build(), 235 + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 236 + contentDescription = "Avatar", 237 + modifier = Modifier 238 + .size(avatarSize) 239 + .offset( 240 + x = when (idx) { 241 + 0 -> 0.dp 242 + else -> (idx * 18).dp 243 + } 244 + ) 245 + .border( 246 + width = 1.dp, 247 + color = MaterialTheme.colorScheme.surfaceContainerLow, 248 + shape = CircleShape 249 + ) 250 + .clip(CircleShape) 306 251 ) 307 252 } 308 253 } 309 254 } 255 + } 256 + 257 + Card( 258 + colors = CardDefaults.cardColors( 259 + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh 260 + ), 261 + modifier = Modifier.padding(start = 28.dp) 262 + ) { 263 + SkeetView( 264 + modifier = Modifier.padding(bottom = 8.dp), 265 + viewModel = null, 266 + skeet = data.post, 267 + nested = true, 268 + showLabels = false, 269 + onShowThread = onShowThread, 270 + ) 310 271 } 311 272 } 312 273 }
+6 -6
app/src/main/java/industries/geesawra/monarch/MainView.kt
··· 322 322 } 323 323 324 324 Scaffold( 325 - containerColor = MaterialTheme.colorScheme.background, 325 + containerColor = MaterialTheme.colorScheme.surface, 326 326 modifier = Modifier 327 327 .fillMaxSize() 328 328 .nestedScroll(scrollBehavior.nestedScrollConnection), 329 329 topBar = { 330 330 TopAppBar( 331 331 colors = TopAppBarDefaults.topAppBarColors( 332 - containerColor = MaterialTheme.colorScheme.background, 333 - scrolledContainerColor = MaterialTheme.colorScheme.background, 334 - navigationIconContentColor = MaterialTheme.colorScheme.onBackground, 335 - titleContentColor = MaterialTheme.colorScheme.onBackground, 336 - actionIconContentColor = MaterialTheme.colorScheme.onBackground, 332 + containerColor = MaterialTheme.colorScheme.surface, 333 + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer, 334 + navigationIconContentColor = MaterialTheme.colorScheme.onSurface, 335 + titleContentColor = MaterialTheme.colorScheme.onSurface, 336 + actionIconContentColor = MaterialTheme.colorScheme.onSurface, 337 337 ), 338 338 title = { 339 339 when (currentDestination) {
+11 -5
app/src/main/java/industries/geesawra/monarch/NotificationsView.kt
··· 16 16 import androidx.compose.material.icons.filled.Add 17 17 import androidx.compose.material3.Card 18 18 import androidx.compose.material3.CardDefaults 19 + import androidx.compose.material3.ElevatedCard 19 20 import androidx.compose.material3.MaterialTheme 20 - import androidx.compose.material3.OutlinedCard 21 21 import androidx.compose.material3.Text 22 22 import androidx.compose.runtime.Composable 23 23 import androidx.compose.runtime.LaunchedEffect ··· 63 63 items = viewModel.uiState.notifications, 64 64 key = { it.createdAt() } 65 65 ) { notif -> 66 - Card( 67 - elevation = CardDefaults.cardElevation( 68 - defaultElevation = if (notif.new() && viewModel.uiState.unreadNotificationsAmt != 0) 8.dp else 0.dp, 66 + ElevatedCard( 67 + elevation = CardDefaults.elevatedCardElevation( 68 + defaultElevation = if (notif.new() && viewModel.uiState.unreadNotificationsAmt != 0) 4.dp else 1.dp, 69 + ), 70 + colors = CardDefaults.elevatedCardColors( 71 + containerColor = MaterialTheme.colorScheme.surfaceContainerLow 69 72 ), 70 73 modifier = Modifier.padding(horizontal = 16.dp) 71 74 ) { ··· 141 144 style = MaterialTheme.typography.bodyLarge, 142 145 ) 143 146 } 144 - OutlinedCard( 147 + Card( 148 + colors = CardDefaults.cardColors( 149 + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh 150 + ), 145 151 modifier = Modifier.padding( 146 152 top = 8.dp, 147 153 start = 40.dp,
+8 -4
app/src/main/java/industries/geesawra/monarch/ShowSkeets.kt
··· 17 17 import androidx.compose.foundation.lazy.itemsIndexed 18 18 import androidx.compose.foundation.lazy.rememberLazyListState 19 19 import androidx.compose.foundation.shape.RoundedCornerShape 20 - import androidx.compose.material3.Card 20 + import androidx.compose.material3.CardDefaults 21 + import androidx.compose.material3.ElevatedCard 21 22 import androidx.compose.material3.HorizontalDivider 22 23 import androidx.compose.material3.VerticalDivider 23 24 import androidx.compose.runtime.Composable ··· 76 77 items = data.filter { !it.replyToNotFollowing && it.cid !in threadContextCids }, 77 78 key = { _, skeet -> skeet.key() } 78 79 ) { idx, skeet -> 79 - Card( 80 - modifier = Modifier.padding(start = (skeet.nestingLevel * 16).dp) 80 + ElevatedCard( 81 + modifier = Modifier.padding(start = (skeet.nestingLevel * 16).dp), 82 + colors = CardDefaults.elevatedCardColors( 83 + containerColor = MaterialTheme.colorScheme.surfaceContainerLow 84 + ), 81 85 ) { 82 86 val isRepost = when (skeet.reason) { 83 87 is FeedViewPostReasonUnion.ReasonRepost -> true ··· 116 120 ) { 117 121 Box( 118 122 modifier = Modifier 119 - .width(44.dp) 123 + .width(40.dp) 120 124 .fillMaxHeight(), 121 125 contentAlignment = Alignment.Center 122 126 ) {
+47 -86
app/src/main/java/industries/geesawra/monarch/SkeetView.kt
··· 41 41 import androidx.compose.material3.Icon 42 42 import androidx.compose.material3.MaterialTheme 43 43 import androidx.compose.material3.OutlinedCard 44 + import androidx.compose.material3.SuggestionChip 45 + import androidx.compose.material3.SuggestionChipDefaults 44 46 import androidx.compose.material3.VerticalDivider 45 47 import androidx.compose.material3.ModalBottomSheet 46 48 import androidx.compose.material3.Surface ··· 115 117 return 116 118 } 117 119 118 - val minSize = 44.dp 120 + val minSize = 40.dp 119 121 120 122 if (nested) { 121 123 // Embedded posts: simple stacked layout ··· 702 704 val isBot = skeet.authorLabels.any { it.`val` == "bot" } 703 705 704 706 Column(modifier = modifier) { 705 - Row(verticalAlignment = Alignment.CenterVertically) { 707 + Row( 708 + verticalAlignment = Alignment.CenterVertically, 709 + modifier = Modifier.fillMaxWidth() 710 + ) { 706 711 Text( 707 712 text = authorName, 708 713 color = MaterialTheme.colorScheme.onSurface, 709 714 style = MaterialTheme.typography.titleSmall, 710 - fontWeight = FontWeight.Bold 715 + fontWeight = FontWeight.Bold, 716 + modifier = Modifier.weight(1f, fill = false) 711 717 ) 712 718 if (skeet.verified) { 713 719 Spacer(modifier = Modifier.width(4.dp)) ··· 727 733 tint = MaterialTheme.colorScheme.onSurfaceVariant 728 734 ) 729 735 } 736 + skeet.createdAt?.let { 737 + Spacer(modifier = Modifier.weight(1f)) 738 + Text( 739 + text = HumanReadable.timeAgo(it), 740 + color = MaterialTheme.colorScheme.onSurfaceVariant, 741 + style = MaterialTheme.typography.labelSmall, 742 + ) 743 + } 730 744 } 731 745 732 746 Text( ··· 760 774 } 761 775 val description = labelDescription(it) 762 776 763 - val labelCard = @Composable { 764 - OutlinedCard( 765 - modifier = Modifier, 766 - shape = CircleShape 767 - ) { 768 - Row( 769 - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), 770 - verticalAlignment = Alignment.CenterVertically 771 - ) { 772 - val avatarUrl = labelerAvatar(it) 773 - if (avatarUrl != null) { 774 - AsyncImage( 775 - model = ImageRequest.Builder(LocalContext.current) 776 - .data(avatarUrl) 777 - .crossfade(true) 778 - .build(), 779 - placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 780 - contentDescription = definition.plaintext, 781 - modifier = Modifier 782 - .size(14.dp) 783 - .clip(CircleShape) 784 - ) 785 - } else { 786 - Icon( 787 - imageVector = definition.icon, 788 - contentDescription = definition.plaintext, 789 - modifier = Modifier.size(14.dp), 790 - tint = MaterialTheme.colorScheme.onSurfaceVariant 791 - ) 792 - } 793 - Spacer(modifier = Modifier.width(4.dp)) 794 - Text( 795 - text = definition.plaintext, 796 - style = MaterialTheme.typography.labelSmall, 797 - ) 798 - } 777 + val chipIcon: @Composable () -> Unit = { 778 + val avatarUrl = labelerAvatar(it) 779 + if (avatarUrl != null) { 780 + AsyncImage( 781 + model = ImageRequest.Builder(LocalContext.current) 782 + .data(avatarUrl) 783 + .crossfade(true) 784 + .build(), 785 + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 786 + contentDescription = definition.plaintext, 787 + modifier = Modifier 788 + .size(SuggestionChipDefaults.IconSize) 789 + .clip(CircleShape) 790 + ) 791 + } else { 792 + Icon( 793 + imageVector = definition.icon, 794 + contentDescription = definition.plaintext, 795 + modifier = Modifier.size(SuggestionChipDefaults.IconSize), 796 + tint = MaterialTheme.colorScheme.onSurfaceVariant 797 + ) 799 798 } 800 799 } 801 800 ··· 827 826 } 828 827 } 829 828 830 - OutlinedCard( 829 + SuggestionChip( 831 830 onClick = { showSheet = true }, 832 - shape = CircleShape 833 - ) { 834 - Row( 835 - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), 836 - verticalAlignment = Alignment.CenterVertically 837 - ) { 838 - val avatarUrl = labelerAvatar(it) 839 - if (avatarUrl != null) { 840 - AsyncImage( 841 - model = ImageRequest.Builder(LocalContext.current) 842 - .data(avatarUrl) 843 - .crossfade(true) 844 - .build(), 845 - placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 846 - contentDescription = definition.plaintext, 847 - modifier = Modifier 848 - .size(14.dp) 849 - .clip(CircleShape) 850 - ) 851 - } else { 852 - Icon( 853 - imageVector = definition.icon, 854 - contentDescription = definition.plaintext, 855 - modifier = Modifier.size(14.dp), 856 - tint = MaterialTheme.colorScheme.onSurfaceVariant 857 - ) 858 - } 859 - Spacer(modifier = Modifier.width(4.dp)) 860 - Text( 861 - text = definition.plaintext, 862 - style = MaterialTheme.typography.labelSmall, 863 - ) 864 - } 865 - } 831 + label = { Text(text = definition.plaintext) }, 832 + icon = chipIcon, 833 + ) 866 834 } else { 867 - labelCard() 835 + SuggestionChip( 836 + onClick = {}, 837 + label = { Text(text = definition.plaintext) }, 838 + icon = chipIcon, 839 + ) 868 840 } 869 841 } 870 842 } 871 843 } 872 844 } 873 845 874 - skeet.createdAt?.let { 875 - Text( 876 - text = HumanReadable.timeAgo(it), 877 - color = MaterialTheme.colorScheme.outline, 878 - style = MaterialTheme.typography.labelSmall, 879 - textAlign = TextAlign.End, 880 - modifier = Modifier 881 - .fillMaxWidth() 882 - .padding(top = 4.dp) 883 - ) 884 - } 885 846 } 886 847 }
+1 -1
app/src/main/java/industries/geesawra/monarch/TimelinePostActionsView.kt
··· 101 101 val replies = remember { mutableLongStateOf(skeet.replies ?: 0) } 102 102 103 103 Row( 104 - horizontalArrangement = Arrangement.SpaceEvenly, 104 + horizontalArrangement = Arrangement.spacedBy(4.dp), 105 105 verticalAlignment = Alignment.CenterVertically, 106 106 modifier = modifier, 107 107 ) {