diff --git a/android/app/src/main/java/com/emergetools/hackernews/AppActivity.kt b/android/app/src/main/java/com/emergetools/hackernews/AppActivity.kt
index 280354cd..ad9d1b20 100644
--- a/android/app/src/main/java/com/emergetools/hackernews/AppActivity.kt
+++ b/android/app/src/main/java/com/emergetools/hackernews/AppActivity.kt
@@ -79,7 +79,7 @@ fun App() {
selected = navItem.selected,
colors = NavigationBarItemDefaults.colors(
selectedIconColor = MaterialTheme.colorScheme.primary,
- indicatorColor = MaterialTheme.colorScheme.primaryContainer
+ indicatorColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)
),
onClick = {
model.actions(AppAction.NavItemSelected(navItem))
diff --git a/android/app/src/main/java/com/emergetools/hackernews/features/bookmarks/BookmarksScreen.kt b/android/app/src/main/java/com/emergetools/hackernews/features/bookmarks/BookmarksScreen.kt
index 327b7203..4bc6421a 100644
--- a/android/app/src/main/java/com/emergetools/hackernews/features/bookmarks/BookmarksScreen.kt
+++ b/android/app/src/main/java/com/emergetools/hackernews/features/bookmarks/BookmarksScreen.kt
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -15,6 +16,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.emergetools.hackernews.features.comments.CommentsDestinations
+import com.emergetools.hackernews.features.stories.ListSeparator
import com.emergetools.hackernews.features.stories.StoriesDestinations
import com.emergetools.hackernews.features.stories.StoryItem
import com.emergetools.hackernews.features.stories.StoryRow
@@ -40,7 +42,7 @@ fun BookmarksScreen(
style = MaterialTheme.typography.titleMedium
)
LazyColumn {
- items(items = state.bookmarks, key = { it.id }) { item ->
+ itemsIndexed(items = state.bookmarks, key = {_, item -> item.id }) { index, item ->
StoryRow(
item = item,
onClick = {
@@ -55,6 +57,11 @@ fun BookmarksScreen(
navigator(BookmarksNavigation.GoToComments(CommentsDestinations.Comments(it.id)))
}
)
+ if(index != state.bookmarks.lastIndex) {
+ ListSeparator(
+ lineColor = MaterialTheme.colorScheme.surfaceContainerHighest,
+ )
+ }
}
}
}
diff --git a/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsDomain.kt b/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsDomain.kt
index ccbd8987..742387af 100644
--- a/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsDomain.kt
+++ b/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsDomain.kt
@@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.time.LocalDateTime
import java.time.ZoneOffset
+import java.time.ZonedDateTime
sealed interface CommentsState {
val headerState: HeaderState
@@ -40,6 +41,7 @@ sealed interface CommentsState {
val title: String,
val author: String,
val points: Int,
+ val timeLabel: String,
val body: String?,
val loggedIn: Boolean,
val upvoted: Boolean,
@@ -53,6 +55,7 @@ sealed interface CommentsState {
title = title,
author = author,
points = points,
+ timeLabel = timeLabel,
body = body,
upvoted = upvoted,
upvoteUrl = upvoteUrl
@@ -104,6 +107,7 @@ sealed interface HeaderState {
val title: String,
val author: String,
val points: Int,
+ val timeLabel: String,
val body: String?,
val upvoted: Boolean,
val upvoteUrl: String,
@@ -179,6 +183,11 @@ class CommentsViewModel(
title = searchResponse.item.title ?: "",
author = searchResponse.item.author ?: "",
points = searchResponse.item.points ?: 0,
+ timeLabel = relativeTimeStamp(
+ epochSeconds = ZonedDateTime
+ .parse(searchResponse.item.createdAt)
+ .toEpochSecond()
+ ),
body = searchResponse.item.text,
loggedIn = loggedIn,
upvoted = postPage.postInfo.upvoted,
diff --git a/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsScreen.kt b/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsScreen.kt
index d0c415b1..a9f3a6e2 100644
--- a/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsScreen.kt
+++ b/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsScreen.kt
@@ -1,12 +1,19 @@
package com.emergetools.hackernews.features.comments
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -24,15 +31,13 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ThumbUp
-import androidx.compose.material.icons.rounded.ThumbUp
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -49,10 +54,14 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import com.emergetools.hackernews.R
+import com.emergetools.hackernews.features.stories.MetadataButton
+import com.emergetools.hackernews.features.stories.MetadataTag
import com.emergetools.hackernews.ui.theme.HackerGreen
import com.emergetools.hackernews.ui.theme.HackerNewsTheme
import com.emergetools.hackernews.ui.theme.HackerOrange
+import com.emergetools.hackernews.ui.theme.HackerRed
@Composable
fun CommentsScreen(
@@ -67,7 +76,8 @@ fun CommentsScreen(
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
- verticalArrangement = Arrangement.spacedBy(8.dp)
+ contentPadding = PaddingValues(8.dp),
+ verticalArrangement = Arrangement.spacedBy(4.dp)
) {
item {
ItemHeader(
@@ -105,7 +115,7 @@ fun CommentsScreen(
strokeWidth = 4f,
cap = StrokeCap.Round,
pathEffect = PathEffect.dashPathEffect(
- intervals = floatArrayOf(20f, 20f)
+ intervals = floatArrayOf(10f, 10f)
)
)
}
@@ -173,7 +183,8 @@ private fun CommentsScreenPreview() {
title = "Show HN: A new HN client for Android",
author = "rikinm",
points = 69,
- body = null,
+ timeLabel = "2h ago",
+ body = "Hello There",
loggedIn = false,
upvoted = false,
upvoteUrl = "",
@@ -188,18 +199,17 @@ private fun CommentsScreenPreview() {
timeLabel = "2d ago",
upvoted = false,
upvoteUrl = "",
- children = listOf(
- CommentState.Content(
- id = 2,
- level = 1,
- author = "vasantm",
- content = "Hello Parent",
- timeLabel = "1h ago",
- upvoted = false,
- upvoteUrl = "",
- children = listOf()
- )
- )
+ children = listOf()
+ ),
+ CommentState.Content(
+ id = 2,
+ level = 1,
+ author = "vasantm",
+ content = "Hello Parent",
+ timeLabel = "1h ago",
+ upvoted = false,
+ upvoteUrl = "",
+ children = listOf()
)
)
),
@@ -233,8 +243,8 @@ fun CommentRow(
.padding(start = startPadding)
.fillMaxWidth()
.heightIn(min = 80.dp)
- .clip(RoundedCornerShape(topStart = 8.dp, bottomStart = 8.dp))
- .background(color = MaterialTheme.colorScheme.surfaceContainer)
+ .clip(RoundedCornerShape(8.dp))
+ .background(color = MaterialTheme.colorScheme.surface)
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
@@ -246,22 +256,21 @@ fun CommentRow(
verticalAlignment = Alignment.CenterVertically
) {
Text(
- text = state.author,
- style = MaterialTheme.typography.labelSmall,
- color = HackerOrange,
- fontWeight = FontWeight.Medium
- )
- Text(
- text = "•",
- style = MaterialTheme.typography.labelSmall,
- color = MaterialTheme.colorScheme.onSurface
- )
- Text(
- text = state.timeLabel,
+ text = "@${state.author}",
style = MaterialTheme.typography.labelSmall,
- fontWeight = FontWeight.Medium,
- color = Color.Gray
+ color = MaterialTheme.colorScheme.onSurface,
+ fontWeight = FontWeight.Bold
)
+ MetadataTag(
+ label = state.timeLabel
+ ) {
+ Icon(
+ modifier = Modifier.size(12.dp),
+ painter = painterResource(R.drawable.ic_time_outline),
+ tint = MaterialTheme.colorScheme.onSurface,
+ contentDescription = "Time posted"
+ )
+ }
Spacer(modifier = Modifier.weight(1f))
Box(
modifier = Modifier
@@ -274,13 +283,13 @@ fun CommentRow(
MaterialTheme.colorScheme.surfaceContainerHighest
}
)
- .padding(vertical = 4.dp, horizontal = 8.dp)
+ .padding(4.dp)
.clickable { onLikeTapped(state) },
contentAlignment = Alignment.Center
) {
Icon(
modifier = Modifier.size(12.dp),
- imageVector = Icons.Default.ThumbUp,
+ painter = painterResource(R.drawable.ic_upvote),
tint = if (state.upvoted) {
HackerGreen
} else {
@@ -294,53 +303,86 @@ fun CommentRow(
Text(
text = state.content.parseAsHtml(),
style = MaterialTheme.typography.labelSmall,
+ fontWeight = FontWeight.Normal,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
is CommentState.Loading -> {
+ val infiniteTransition = rememberInfiniteTransition("Skeleton")
+ val skeletonAlpha by infiniteTransition.animateFloat(
+ initialValue = 0.2f,
+ targetValue = 0.6f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(durationMillis = 1000, easing = LinearEasing),
+ repeatMode = RepeatMode.Reverse,
+ ),
+ label = "Skeleton Alpha"
+ )
Row(
- horizontalArrangement = Arrangement.spacedBy(4.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.width(40.dp)
- .height(14.dp)
+ .height(12.dp)
.clip(RoundedCornerShape(4.dp))
- .background(HackerOrange)
+ .background(color = MaterialTheme.colorScheme.onSurface.copy(alpha = skeletonAlpha))
)
+ Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
+ Box(
+ modifier = Modifier
+ .size(12.dp)
+ .clip(RoundedCornerShape(4.dp))
+ .background(color = MaterialTheme.colorScheme.onSurface.copy(alpha = skeletonAlpha))
+ )
+ Box(
+ modifier = Modifier
+ .size(12.dp)
+ .clip(RoundedCornerShape(4.dp))
+ .background(color = MaterialTheme.colorScheme.onSurface.copy(alpha = skeletonAlpha))
+ )
+ }
+ Spacer(modifier = Modifier.weight(1f))
Box(
modifier = Modifier
- .width(40.dp)
- .height(14.dp)
- .clip(RoundedCornerShape(4.dp))
- .background(Color.Gray)
- )
+ .clip(CircleShape)
+ .background(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f))
+ .padding(4.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Box(
+ modifier = Modifier
+ .size(12.dp)
+ .clip(CircleShape)
+ .background(color = MaterialTheme.colorScheme.onSurface.copy(alpha = skeletonAlpha))
+ )
+ }
}
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Box(
modifier = Modifier
.fillMaxWidth()
- .height(14.dp)
+ .height(12.dp)
.clip(RoundedCornerShape(4.dp))
- .background(Color.LightGray)
+ .background(MaterialTheme.colorScheme.onSurface.copy(alpha = skeletonAlpha))
)
Box(
modifier = Modifier
.fillMaxWidth()
- .height(14.dp)
+ .height(12.dp)
.clip(RoundedCornerShape(4.dp))
- .background(Color.LightGray)
+ .background(MaterialTheme.colorScheme.onSurface.copy(alpha = skeletonAlpha))
)
Box(
modifier = Modifier
.fillMaxWidth(0.75f)
- .height(14.dp)
+ .height(12.dp)
.clip(RoundedCornerShape(4.dp))
- .background(Color.LightGray)
+ .background(MaterialTheme.colorScheme.onSurface.copy(alpha = skeletonAlpha))
)
}
}
@@ -369,18 +411,7 @@ fun CommentRowPreview() {
timeLabel = "2d ago",
upvoted = false,
upvoteUrl = "",
- children = listOf(
- CommentState.Content(
- id = 2,
- level = 1,
- author = "vasantm",
- content = "Hello Child",
- timeLabel = "2h ago",
- upvoted = false,
- upvoteUrl = "",
- children = listOf()
- )
- )
+ children = listOf()
),
onLikeTapped = {}
)
@@ -408,136 +439,155 @@ fun ItemHeader(
onLikeTapped: (HeaderState.Content) -> Unit,
) {
Column(
- modifier = modifier
- .background(color = MaterialTheme.colorScheme.background)
- .padding(8.dp),
- verticalArrangement = Arrangement.spacedBy(16.dp)
+ modifier = modifier.background(color = MaterialTheme.colorScheme.background),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
) {
when (state) {
is HeaderState.Content -> {
- Text(
- text = state.title,
- color = MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.titleSmall
- )
- if (state.body != null) {
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(
- text = state.body.parseAsHtml(),
+ text = state.title,
color = MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.labelSmall,
+ style = MaterialTheme.typography.titleSmall,
+ fontSize = 20.sp
)
- }
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(4.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
Row(
- modifier = Modifier
- .wrapContentSize()
- .clip(CircleShape)
- .background(
- color = if (state.upvoted) {
- HackerGreen.copy(alpha = 0.2f)
- } else {
- MaterialTheme.colorScheme.surfaceContainerHighest
- }
- )
- .padding(horizontal = 8.dp, vertical = 4.dp)
- .clickable { onLikeTapped(state) },
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(4.dp)
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
) {
- Icon(
- modifier = Modifier.size(12.dp),
- imageVector = Icons.Rounded.ThumbUp,
- tint = if (state.upvoted) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(2.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = "@${state.author}",
+ color = HackerOrange,
+ style = MaterialTheme.typography.labelSmall,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ MetadataTag(label = state.timeLabel) {
+ Icon(
+ modifier = Modifier.size(12.dp),
+ painter = painterResource(R.drawable.ic_time_outline),
+ tint = HackerRed,
+ contentDescription = "Time Posted"
+ )
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ MetadataButton(
+ label = "${state.points}",
+ contentColor = if (state.upvoted) {
HackerGreen
} else {
MaterialTheme.colorScheme.onSurface
},
- contentDescription = "Upvote"
- )
+ onClick = { onLikeTapped(state) }
+ ) {
+ Icon(
+ modifier = Modifier.size(12.dp),
+ painter = painterResource(R.drawable.ic_upvote),
+ tint = HackerGreen,
+ contentDescription = "Upvotes"
+ )
+ }
+ }
+ }
+ if (state.body != null) {
+ Box(
+ Modifier
+ .fillMaxWidth()
+ .heightIn(min = 40.dp)
+ .clip(RoundedCornerShape(8.dp))
+ .background(color = MaterialTheme.colorScheme.surface)
+ .padding(8.dp),
+ contentAlignment = Alignment.CenterStart
+ ) {
Text(
- text = "${state.points}",
- color = if (state.upvoted) {
- HackerGreen
- } else {
- MaterialTheme.colorScheme.onSurface
- },
- style = MaterialTheme.typography.labelSmall
+ text = state.body.parseAsHtml(),
+ color = MaterialTheme.colorScheme.onBackground,
+ style = MaterialTheme.typography.labelSmall,
)
}
- Text(
- text = "•",
- color = MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.labelSmall
- )
- Text(
- text = state.author,
- color = MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.labelSmall,
- fontWeight = FontWeight.Medium
- )
}
}
HeaderState.Loading -> {
- Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .height(18.dp)
- .clip(RoundedCornerShape(4.dp))
- .background(color = MaterialTheme.colorScheme.onBackground)
- )
- Box(
- modifier = Modifier
- .fillMaxWidth(0.75f)
- .height(18.dp)
- .clip(RoundedCornerShape(4.dp))
- .background(color = MaterialTheme.colorScheme.onBackground)
- )
- }
- Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .height(14.dp)
- .clip(RoundedCornerShape(4.dp))
- .background(color = Color.LightGray)
- )
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .height(14.dp)
- .clip(RoundedCornerShape(4.dp))
- .background(color = Color.LightGray)
- )
- Box(
- modifier = Modifier
- .fillMaxWidth(0.75f)
- .height(14.dp)
- .clip(RoundedCornerShape(4.dp))
- .background(color = Color.LightGray)
- )
+ val infiniteTransition = rememberInfiniteTransition("Skeleton Loader")
+ val skeletonAlpha by infiniteTransition.animateFloat(
+ initialValue = 0.2f,
+ targetValue = 0.6f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(durationMillis = 1000, easing = LinearEasing),
+ repeatMode = RepeatMode.Reverse
+ ),
+ label = "Skeleton Alpha"
+ )
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(18.dp)
+ .clip(RoundedCornerShape(6.dp))
+ .background(color = MaterialTheme.colorScheme.onBackground.copy(alpha = skeletonAlpha))
+ )
+ Box(
+ modifier = Modifier
+ .fillMaxWidth(0.75f)
+ .height(18.dp)
+ .clip(RoundedCornerShape(6.dp))
+ .background(color = MaterialTheme.colorScheme.onBackground.copy(alpha = skeletonAlpha))
+ )
+ }
}
-
- Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
Box(
modifier = Modifier
- .width(30.dp)
- .height(14.dp)
+ .width(60.dp)
+ .height(12.dp)
.clip(RoundedCornerShape(4.dp))
- .background(Color.DarkGray)
+ .background(HackerOrange.copy(alpha = skeletonAlpha))
)
- Box(
+ Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
+ Box(
+ modifier = Modifier
+ .size(12.dp)
+ .clip(RoundedCornerShape(4.dp))
+ .background(HackerRed.copy(alpha = skeletonAlpha))
+ )
+ Box(
+ modifier = Modifier
+ .size(12.dp)
+ .clip(RoundedCornerShape(4.dp))
+ .background(MaterialTheme.colorScheme.onBackground.copy(alpha = skeletonAlpha))
+ )
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ Row(
modifier = Modifier
- .width(30.dp)
- .height(14.dp)
- .clip(RoundedCornerShape(4.dp))
- .background(MaterialTheme.colorScheme.onBackground)
- )
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.onBackground.copy(alpha = 0.1f))
+ .padding(vertical = 4.dp, horizontal = 8.dp),
+ horizontalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .size(12.dp)
+ .clip(RoundedCornerShape(4.dp))
+ .background(HackerGreen.copy(alpha = skeletonAlpha))
+ )
+ Box(
+ modifier = Modifier
+ .size(12.dp)
+ .clip(RoundedCornerShape(4.dp))
+ .background(MaterialTheme.colorScheme.onBackground.copy(alpha = skeletonAlpha))
+ )
+ }
}
}
}
@@ -548,21 +598,28 @@ fun ItemHeader(
@Composable
private fun ItemHeaderPreview() {
HackerNewsTheme {
- ItemHeader(
- state = HeaderState.Content(
- id = 0L,
- title = "Show HN: A super neat HN client for Android",
- author = "rikinm",
- points = 69,
- body = "Hi there",
- upvoted = false,
- upvoteUrl = "",
- ),
+ Box(
modifier = Modifier
- .fillMaxWidth()
- .wrapContentHeight(),
- onLikeTapped = {}
- )
+ .background(MaterialTheme.colorScheme.background)
+ .padding(8.dp)
+ ) {
+ ItemHeader(
+ state = HeaderState.Content(
+ id = 0L,
+ title = "Show HN: A super neat HN client for Android",
+ author = "rikinm",
+ points = 69,
+ timeLabel = "2h ago",
+ body = "Wassup HN. I just built a sick new Hacker News Android client",
+ upvoted = false,
+ upvoteUrl = "",
+ ),
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ onLikeTapped = {}
+ )
+ }
}
}
@@ -570,13 +627,19 @@ private fun ItemHeaderPreview() {
@Composable
private fun ItemHeaderLoadingPreview() {
HackerNewsTheme {
- ItemHeader(
- state = HeaderState.Loading,
+ Box(
modifier = Modifier
- .fillMaxWidth()
- .wrapContentHeight(),
- onLikeTapped = {}
- )
+ .background(MaterialTheme.colorScheme.background)
+ .padding(8.dp)
+ ) {
+ ItemHeader(
+ state = HeaderState.Loading,
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ onLikeTapped = {}
+ )
+ }
}
}
diff --git a/android/app/src/main/java/com/emergetools/hackernews/features/settings/SettingsScreen.kt b/android/app/src/main/java/com/emergetools/hackernews/features/settings/SettingsScreen.kt
index 4128d9b0..6bc703bf 100644
--- a/android/app/src/main/java/com/emergetools/hackernews/features/settings/SettingsScreen.kt
+++ b/android/app/src/main/java/com/emergetools/hackernews/features/settings/SettingsScreen.kt
@@ -214,7 +214,7 @@ fun LoginCard(
scaleY = iconScale
}
.size(12.dp),
- imageVector = Icons.Rounded.ThumbUp,
+ painter = painterResource(R.drawable.ic_upvote),
tint = likeColor,
contentDescription = "Likes"
)
diff --git a/android/app/src/main/java/com/emergetools/hackernews/features/stories/StoriesScreen.kt b/android/app/src/main/java/com/emergetools/hackernews/features/stories/StoriesScreen.kt
index 7b88637f..c91fa2e9 100644
--- a/android/app/src/main/java/com/emergetools/hackernews/features/stories/StoriesScreen.kt
+++ b/android/app/src/main/java/com/emergetools/hackernews/features/stories/StoriesScreen.kt
@@ -1,8 +1,14 @@
package com.emergetools.hackernews.features.stories
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -12,19 +18,20 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.layout.wrapContentWidth
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.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Refresh
@@ -53,19 +60,19 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.center
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
-import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.tooling.preview.Devices
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.emergetools.hackernews.R
import com.emergetools.hackernews.features.comments.CommentsDestinations
import com.emergetools.hackernews.ui.preview.AppStoreSnapshot
+import com.emergetools.hackernews.ui.theme.HackerBlue
+import com.emergetools.hackernews.ui.theme.HackerGreen
import com.emergetools.hackernews.ui.theme.HackerNewsTheme
import com.emergetools.hackernews.ui.theme.HackerOrange
import com.emergetools.hackernews.ui.theme.HackerRed
@@ -124,7 +131,7 @@ fun StoriesScreen(
}
}
}
- items(state.stories) { item ->
+ itemsIndexed(state.stories) { index, item ->
StoryRow(
modifier = Modifier.animateItem(),
item = item,
@@ -154,6 +161,11 @@ fun StoriesScreen(
)
},
)
+ if (index != state.stories.lastIndex) {
+ ListSeparator(
+ lineColor = MaterialTheme.colorScheme.surfaceContainerHighest,
+ )
+ }
}
}
}
@@ -215,156 +227,194 @@ fun StoryRow(
}, stiffness = Spring.StiffnessLow
), label = "Bookmark Height"
)
- Row(modifier = modifier
- .fillMaxWidth()
- .heightIn(min = 80.dp)
- .background(color = MaterialTheme.colorScheme.background)
- .clip(shape = RectangleShape)
- .drawWithContent {
- drawContent()
- val startX = size.width * 0.75f
- val startY = 0f
- val bookmarkWidth = 50f
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .heightIn(min = 100.dp)
+ .background(color = MaterialTheme.colorScheme.background)
+ .drawWithContent {
+ drawContent()
+ val startX = size.width * 0.75f
+ val startY = 0f
+ val bookmarkWidth = 50f
- val path = Path().apply {
- moveTo(startX, startY)
- lineTo(startX, startY + bookmarkHeight)
- lineTo(startX + bookmarkWidth / 2f, startY + bookmarkHeight * 0.75f)
- lineTo(startX + bookmarkWidth, startY + bookmarkHeight)
- lineTo(startX + bookmarkWidth, startY)
- }
+ val path = Path().apply {
+ moveTo(startX, startY)
+ lineTo(startX, startY + bookmarkHeight)
+ lineTo(startX + bookmarkWidth / 2f, startY + bookmarkHeight * 0.75f)
+ lineTo(startX + bookmarkWidth, startY + bookmarkHeight)
+ lineTo(startX + bookmarkWidth, startY)
+ }
- drawPath(
- path,
- color = HackerOrange,
- )
- }
- .combinedClickable(onClick = {
- onClick(item)
- }, onLongClick = {
- onBookmark(item)
- })
- .padding(8.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(
- 16.dp, alignment = Alignment.CenterHorizontally
+ drawPath(
+ path,
+ color = HackerOrange,
+ )
+ }
+ .combinedClickable(onClick = {
+ onClick(item)
+ }, onLongClick = {
+ onBookmark(item)
+ })
+ .padding(8.dp),
+ verticalArrangement = Arrangement.spacedBy(
+ space = 8.dp,
+ alignment = Alignment.CenterVertically
)
) {
- Column(
- modifier = Modifier
- .wrapContentHeight()
- .weight(1f),
- verticalArrangement = Arrangement.Center
+ Text(
+ text = "@${item.author}",
+ color = HackerOrange,
+ style = MaterialTheme.typography.labelSmall,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ text = item.title,
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.titleSmall
+ )
+ Spacer(modifier = Modifier.height(0.dp))
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
) {
- Text(
- text = item.title,
- color = MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.titleSmall
- )
- Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
- Text(
- text = "${item.score}",
- color = MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.labelSmall
- )
- Text(
- text = "•",
- color = MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.labelSmall
- )
- Text(
- text = item.author,
- color = HackerOrange,
- style = MaterialTheme.typography.labelSmall,
- fontWeight = FontWeight.Medium
+ MetadataTag(
+ label = "${item.score}",
+ ) {
+ Icon(
+ modifier = Modifier.size(12.dp),
+ painter = painterResource(R.drawable.ic_upvote),
+ tint = HackerGreen,
+ contentDescription = "Likes"
)
- Text(
- text = "•",
- color = MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.labelSmall
+ }
+ MetadataTag(
+ label = item.timeLabel,
+ ) {
+ Icon(
+ modifier = Modifier.size(12.dp),
+ painter = painterResource(R.drawable.ic_time_outline),
+ tint = HackerRed,
+ contentDescription = "Likes"
)
- Text(
- text = item.timeLabel,
- color = MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.labelSmall
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ MetadataButton(
+ label = "${item.commentCount}",
+ onClick = { onCommentClicked(item) }
+ ) {
+ Icon(
+ modifier = Modifier.size(12.dp),
+ painter = painterResource(R.drawable.ic_chat),
+ tint = HackerBlue,
+ contentDescription = "Comments"
)
}
}
-
- Column(
- modifier = Modifier
- .padding(horizontal = 16.dp)
- .clickable {
- onCommentClicked(item)
- },
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Icon(
- modifier = Modifier.size(20.dp),
- painter = painterResource(R.drawable.ic_chat),
- tint = MaterialTheme.colorScheme.onBackground,
- contentDescription = ""
- )
- Text(
- text = "${item.commentCount}",
- color = MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.labelSmall,
- fontWeight = FontWeight.Medium
- )
- }
}
}
is StoryItem.Loading -> {
- Row(
+ val infiniteTransition = rememberInfiniteTransition("Skeleton")
+ val skeletonAlpha by infiniteTransition.animateFloat(
+ initialValue = 0.2f,
+ targetValue = 0.6f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(durationMillis = 1000, easing = LinearEasing),
+ repeatMode = RepeatMode.Reverse,
+ ),
+ label = "Skeleton Alpha"
+ )
+ Column(
modifier = modifier
.fillMaxWidth()
- .heightIn(min = 80.dp)
+ .heightIn(min = 100.dp)
.background(color = MaterialTheme.colorScheme.background)
.padding(8.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(
- 16.dp, alignment = Alignment.CenterHorizontally
- )
+ verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically)
) {
- Column(
+ Box(
modifier = Modifier
- .wrapContentHeight()
- .weight(1f),
- verticalArrangement = Arrangement.spacedBy(4.dp)
- ) {
+ .fillMaxWidth(0.15f)
+ .height(12.dp)
+ .clip(RoundedCornerShape(4.dp))
+ .background(color = HackerOrange.copy(alpha = skeletonAlpha))
+ )
+ Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Box(
modifier = Modifier
- .fillMaxWidth(0.8f)
- .height(18.dp)
+ .fillMaxWidth(0.75f)
+ .height(12.dp)
.clip(RoundedCornerShape(4.dp))
- .background(color = Color.LightGray)
+ .background(color = MaterialTheme.colorScheme.onSurface.copy(alpha = skeletonAlpha))
)
Box(
modifier = Modifier
- .fillMaxWidth(0.45f)
- .height(18.dp)
+ .fillMaxWidth(0.5f)
+ .height(12.dp)
.clip(RoundedCornerShape(4.dp))
- .background(color = Color.Gray)
+ .background(color = MaterialTheme.colorScheme.onSurface.copy(alpha = skeletonAlpha))
)
+ }
+ Spacer(modifier = Modifier.height(0.dp))
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(
+ modifier = Modifier.wrapContentSize(),
+ horizontalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .size(12.dp)
+ .clip(RoundedCornerShape(4.dp))
+ .background(color = HackerGreen.copy(alpha = skeletonAlpha))
+ )
+ Box(
+ modifier = Modifier
+ .size(12.dp)
+ .clip(RoundedCornerShape(4.dp))
+ .background(color = MaterialTheme.colorScheme.onSurface.copy(alpha = skeletonAlpha))
+ )
+ }
+ Row(
+ modifier = Modifier.wrapContentSize(),
+ horizontalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .size(12.dp)
+ .clip(RoundedCornerShape(4.dp))
+ .background(color = HackerRed.copy(alpha = skeletonAlpha))
+ )
+ Box(
+ modifier = Modifier
+ .size(12.dp)
+ .clip(RoundedCornerShape(4.dp))
+ .background(color = MaterialTheme.colorScheme.onSurface.copy(alpha = skeletonAlpha))
+ )
+ }
+ Spacer(modifier = Modifier.weight(1f))
Row(
- horizontalArrangement = Arrangement.spacedBy(4.dp),
- verticalAlignment = Alignment.CenterVertically
+ modifier = Modifier
+ .wrapContentSize()
+ .clip(CircleShape)
+ .background(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f))
+ .padding(vertical = 4.dp, horizontal = 8.dp),
+ horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
Box(
modifier = Modifier
- .width(30.dp)
- .height(14.dp)
+ .size(12.dp)
.clip(RoundedCornerShape(4.dp))
- .background(Color.DarkGray)
+ .background(color = HackerBlue.copy(alpha = skeletonAlpha))
)
Box(
modifier = Modifier
- .width(40.dp)
- .height(14.dp)
+ .size(12.dp)
.clip(RoundedCornerShape(4.dp))
- .background(HackerOrange)
+ .background(color = MaterialTheme.colorScheme.onSurface.copy(alpha = skeletonAlpha))
)
}
}
@@ -373,6 +423,68 @@ fun StoryRow(
}
}
+@Composable
+fun ListSeparator(
+ lineColor: Color,
+ space: Dp = 0.5.dp
+) {
+ Spacer(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(space)
+ .background(color = lineColor)
+ )
+}
+
+@Composable
+fun MetadataButton(
+ label: String,
+ contentColor: Color = MaterialTheme.colorScheme.onSurface,
+ backgroundColor: Color = contentColor.copy(alpha = 0.1f),
+ onClick: () -> Unit = {},
+ icon: @Composable () -> Unit,
+) {
+ Row(
+ modifier = Modifier
+ .wrapContentSize()
+ .clip(CircleShape)
+ .clickable { onClick() }
+ .background(color = backgroundColor)
+ .padding(vertical = 4.dp, horizontal = 8.dp),
+ horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ icon()
+ Text(
+ text = label,
+ style = MaterialTheme.typography.labelSmall,
+ fontWeight = FontWeight.Medium,
+ color = contentColor
+ )
+ }
+}
+
+@Composable
+fun MetadataTag(
+ label: String,
+ contentColor: Color = MaterialTheme.colorScheme.onSurface,
+ icon: @Composable () -> Unit
+) {
+ Row(
+ modifier = Modifier.wrapContentSize(),
+ horizontalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterHorizontally),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ icon()
+ Text(
+ text = label,
+ style = MaterialTheme.typography.labelSmall,
+ fontWeight = FontWeight.Medium,
+ color = contentColor
+ )
+ }
+}
+
@PreviewLightDark
@Composable
private fun StoryRowPreview() {
@@ -493,8 +605,7 @@ private fun FeedSelection(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
- .clickable(
- indication = null,
+ .clickable(indication = null,
interactionSource = remember { MutableInteractionSource() }) {
onSelected(feedType)
},
diff --git a/android/app/src/main/java/com/emergetools/hackernews/ui/theme/Type.kt b/android/app/src/main/java/com/emergetools/hackernews/ui/theme/Type.kt
index 84f898cb..b785b6a3 100644
--- a/android/app/src/main/java/com/emergetools/hackernews/ui/theme/Type.kt
+++ b/android/app/src/main/java/com/emergetools/hackernews/ui/theme/Type.kt
@@ -1,6 +1,5 @@
package com.emergetools.hackernews.ui.theme
-import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
@@ -19,7 +18,7 @@ val Typography = Typography(
titleSmall = TextStyle(
fontFamily = plex,
fontWeight = FontWeight.Bold,
- fontSize = 18.sp
+ fontSize = 16.sp
),
titleMedium = TextStyle(
fontFamily = plex,
@@ -29,12 +28,12 @@ val Typography = Typography(
labelSmall = TextStyle(
fontFamily = plex,
fontWeight = FontWeight.Normal,
- fontSize = 14.sp
+ fontSize = 12.sp
),
labelMedium = TextStyle(
fontFamily = plex,
fontWeight = FontWeight.Normal,
- fontSize = 16.sp
+ fontSize = 14.sp
),
/* Other default text styles to override
titleLarge = TextStyle(
diff --git a/android/app/src/main/res/drawable/ic_time_outline.xml b/android/app/src/main/res/drawable/ic_time_outline.xml
new file mode 100644
index 00000000..9e13a0ad
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_time_outline.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_upvote.xml b/android/app/src/main/res/drawable/ic_upvote.xml
new file mode 100644
index 00000000..45f227cd
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_upvote.xml
@@ -0,0 +1,10 @@
+
+
+