Skip to content

Commit 019cb1c

Browse files
committed
Add date subheaders to the RS comment list
Interleave date-group headers into the rs comment list, matching the legacy list. A new withDateHeaders() groups consecutive comments by their existing relativeDate label (the same javaDateToTimeSpan value legacy groups by) and inserts a header row on each change; the LazyColumn renders headers and comments as distinct row types.
1 parent 3884179 commit 019cb1c

4 files changed

Lines changed: 146 additions & 10 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.wordpress.android.ui.commentsrs
2+
3+
/** A rendered row in the rs comment list: either a date-group header or a comment. */
4+
sealed interface CommentsRsListRow {
5+
/** A date subheader. [keyId] is the first comment id in the group, for a stable LazyColumn key. */
6+
data class DateHeader(val label: String, val keyId: Long) : CommentsRsListRow
7+
data class Item(val comment: CommentRsUiModel) : CommentsRsListRow
8+
}
9+
10+
/**
11+
* Interleaves date subheaders into [comments] (already in display order), mirroring the legacy
12+
* list: a header before the first comment and before every comment whose date label differs from
13+
* the previous one. The label is the row's own [CommentRsUiModel.relativeDate] — the same
14+
* javaDateToTimeSpan value the legacy list groups by — so no extra date handling is needed here.
15+
*/
16+
fun withDateHeaders(comments: List<CommentRsUiModel>): List<CommentsRsListRow> {
17+
val rows = ArrayList<CommentsRsListRow>(comments.size + 1)
18+
var lastLabel: String? = null
19+
for (comment in comments) {
20+
if (comment.relativeDate != lastLabel) {
21+
rows.add(CommentsRsListRow.DateHeader(comment.relativeDate, comment.remoteCommentId))
22+
lastLabel = comment.relativeDate
23+
}
24+
rows.add(CommentsRsListRow.Item(comment))
25+
}
26+
return rows
27+
}

WordPress/src/main/java/org/wordpress/android/ui/commentsrs/screens/CommentsRsListItem.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,19 @@ private fun AnnotatedString.Builder.boldRange(formatted: String, part: String) {
173173
addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, start + part.length)
174174
}
175175
}
176+
177+
/** A date-group subheader row, matching the legacy list's all-caps overline separators. */
178+
@Composable
179+
fun CommentsRsDateHeader(label: String, modifier: Modifier = Modifier) {
180+
Text(
181+
text = label.uppercase(),
182+
style = MaterialTheme.typography.labelMedium,
183+
color = MaterialTheme.colorScheme.onSurfaceVariant,
184+
maxLines = 1,
185+
overflow = TextOverflow.Ellipsis,
186+
modifier = modifier
187+
.fillMaxWidth()
188+
.background(MaterialTheme.colorScheme.surface)
189+
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp)
190+
)
191+
}

WordPress/src/main/java/org/wordpress/android/ui/commentsrs/screens/CommentsRsTabListScreen.kt

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
2626
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
2727
import androidx.compose.runtime.Composable
2828
import androidx.compose.runtime.LaunchedEffect
29+
import androidx.compose.runtime.remember
2930
import androidx.compose.runtime.snapshotFlow
3031
import androidx.compose.ui.Alignment
3132
import androidx.compose.ui.Modifier
@@ -36,7 +37,9 @@ import androidx.compose.ui.unit.dp
3637
import kotlinx.coroutines.flow.distinctUntilChanged
3738
import org.wordpress.android.R
3839
import org.wordpress.android.ui.commentsrs.CommentRsUiModel
40+
import org.wordpress.android.ui.commentsrs.CommentsRsListRow
3941
import org.wordpress.android.ui.commentsrs.CommentsTabUiState
42+
import org.wordpress.android.ui.commentsrs.withDateHeaders
4043
import org.wordpress.android.ui.compose.components.ShimmerBox
4144

4245
@OptIn(ExperimentalMaterial3Api::class)
@@ -135,21 +138,35 @@ private fun CommentListContent(
135138
}
136139
}
137140

141+
// Interleave date subheaders once per comment-list change, like the legacy list.
142+
val rows = remember(comments) { withDateHeaders(comments) }
138143
LazyColumn(
139144
state = listState,
140145
modifier = Modifier.fillMaxSize()
141146
) {
142147
items(
143-
items = comments,
144-
key = { it.remoteCommentId }
145-
) { comment ->
146-
CommentsRsListItem(
147-
comment = comment,
148-
isSelected = comment.remoteCommentId in selectedIds,
149-
onClick = { onCommentClick(comment.remoteCommentId) },
150-
onLongClick = { onCommentLongClick(comment.remoteCommentId) },
151-
modifier = Modifier.animateItem()
152-
)
148+
items = rows,
149+
key = { row ->
150+
when (row) {
151+
is CommentsRsListRow.DateHeader -> "header_${row.keyId}"
152+
is CommentsRsListRow.Item -> row.comment.remoteCommentId
153+
}
154+
},
155+
contentType = { it::class }
156+
) { row ->
157+
when (row) {
158+
is CommentsRsListRow.DateHeader -> CommentsRsDateHeader(
159+
label = row.label,
160+
modifier = Modifier.animateItem()
161+
)
162+
is CommentsRsListRow.Item -> CommentsRsListItem(
163+
comment = row.comment,
164+
isSelected = row.comment.remoteCommentId in selectedIds,
165+
onClick = { onCommentClick(row.comment.remoteCommentId) },
166+
onLongClick = { onCommentLongClick(row.comment.remoteCommentId) },
167+
modifier = Modifier.animateItem()
168+
)
169+
}
153170
}
154171

155172
if (isLoadingMore) {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package org.wordpress.android.ui.commentsrs
2+
3+
import org.assertj.core.api.Assertions.assertThat
4+
import org.junit.Test
5+
import org.wordpress.android.fluxc.model.CommentStatus
6+
import org.wordpress.android.ui.commentsrs.CommentsRsListRow.DateHeader
7+
import org.wordpress.android.ui.commentsrs.CommentsRsListRow.Item
8+
9+
class CommentsRsListRowTest {
10+
@Test
11+
fun `empty list produces no rows`() {
12+
assertThat(withDateHeaders(emptyList())).isEmpty()
13+
}
14+
15+
@Test
16+
fun `a single comment gets a leading date header`() {
17+
val comment = comment(id = 1, date = "Today")
18+
19+
assertThat(withDateHeaders(listOf(comment))).containsExactly(
20+
DateHeader("Today", keyId = 1),
21+
Item(comment)
22+
)
23+
}
24+
25+
@Test
26+
fun `consecutive comments with the same date share one header`() {
27+
val a = comment(id = 1, date = "Today")
28+
val b = comment(id = 2, date = "Today")
29+
30+
assertThat(withDateHeaders(listOf(a, b))).containsExactly(
31+
DateHeader("Today", keyId = 1),
32+
Item(a),
33+
Item(b)
34+
)
35+
}
36+
37+
@Test
38+
fun `a new header is inserted whenever the date label changes`() {
39+
val a = comment(id = 1, date = "Today")
40+
val b = comment(id = 2, date = "Today")
41+
val c = comment(id = 3, date = "Yesterday")
42+
val d = comment(id = 4, date = "January 8")
43+
44+
assertThat(withDateHeaders(listOf(a, b, c, d))).containsExactly(
45+
DateHeader("Today", keyId = 1),
46+
Item(a),
47+
Item(b),
48+
DateHeader("Yesterday", keyId = 3),
49+
Item(c),
50+
DateHeader("January 8", keyId = 4),
51+
Item(d)
52+
)
53+
}
54+
55+
@Test
56+
fun `each header is keyed by the first comment in its group`() {
57+
val rows = withDateHeaders(
58+
listOf(
59+
comment(id = 10, date = "Today"),
60+
comment(id = 11, date = "Yesterday")
61+
)
62+
)
63+
64+
assertThat(rows.filterIsInstance<DateHeader>().map { it.keyId }).containsExactly(10, 11)
65+
}
66+
67+
private fun comment(id: Long, date: String) = CommentRsUiModel(
68+
remoteCommentId = id,
69+
authorName = "Jane",
70+
avatarUrl = "",
71+
snippet = "hello",
72+
relativeDate = date,
73+
status = CommentStatus.APPROVED,
74+
postId = 99L
75+
)
76+
}

0 commit comments

Comments
 (0)