Skip to content

Commit 5bc3b2e

Browse files
perf: remove nest scroll for better perf
1 parent 5a2e8c0 commit 5bc3b2e

File tree

7 files changed

+393
-322
lines changed

7 files changed

+393
-322
lines changed

android/src/main/kotlin/project/pipepipe/app/ui/list/CommentList.kt

Lines changed: 70 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
1313
import androidx.compose.foundation.layout.height
1414
import androidx.compose.foundation.layout.padding
1515
import androidx.compose.foundation.lazy.LazyColumn
16+
import androidx.compose.foundation.lazy.LazyListScope
1617
import androidx.compose.foundation.lazy.LazyListState
1718
import androidx.compose.foundation.lazy.items
1819
import androidx.compose.material.icons.Icons
@@ -36,113 +37,92 @@ import project.pipepipe.app.ui.item.CommentItem
3637
import project.pipepipe.app.ui.screens.Screen
3738
import project.pipepipe.app.SharedContext
3839

40+
/**
41+
* LazyListScope extension for comment list content (without the LazyColumn wrapper)
42+
* Used by CommentSection to combine with common header
43+
*/
3944
@OptIn(ExperimentalFoundationApi::class)
40-
@Composable
41-
fun CommentList(
45+
fun LazyListScope.commentListContent(
4246
comments: List<CommentInfo>,
4347
isLoading: Boolean,
4448
hasMoreComments: Boolean,
4549
onShowReplies: (CommentInfo) -> Unit,
4650
onLoadMore: () -> Unit,
4751
showStickyHeader: Boolean = false,
4852
onBackClick: () -> Unit = {},
49-
modifier: Modifier = Modifier,
50-
listState: LazyListState,
51-
onTimestampClick: (Long) -> Unit,
52-
navController: NavHostController
53+
navController: NavHostController,
54+
onTimestampClick: (Long) -> Unit
5355
) {
54-
val uniqueItems = remember(comments) { comments.distinctBy { it.url } }
56+
val uniqueItems = comments.distinctBy { it.url }
5557

56-
LaunchedEffect(listState, hasMoreComments, isLoading) {
57-
snapshotFlow {
58-
val layoutInfo = listState.layoutInfo
59-
val lastVisibleIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
60-
val totalItemsCount = layoutInfo.totalItemsCount
61-
lastVisibleIndex to totalItemsCount
58+
if (isLoading && comments.isEmpty()) {
59+
item {
60+
Box(
61+
modifier = Modifier
62+
.fillMaxWidth()
63+
.padding(32.dp),
64+
contentAlignment = Alignment.Center
65+
) {
66+
CircularProgressIndicator()
67+
}
6268
}
63-
.distinctUntilChanged()
64-
.collect { (lastVisibleIndex, totalItemsCount) ->
65-
val isAtBottom = totalItemsCount > 0 && lastVisibleIndex == totalItemsCount - 1
66-
if (isAtBottom && hasMoreComments && !isLoading) {
67-
onLoadMore()
69+
} else {
70+
if (showStickyHeader) {
71+
stickyHeader {
72+
Row(
73+
modifier = Modifier
74+
.fillMaxWidth()
75+
.background(MaterialTheme.colorScheme.surface)
76+
.padding(16.dp),
77+
verticalAlignment = Alignment.CenterVertically,
78+
horizontalArrangement = Arrangement.Start
79+
) {
80+
Icon(
81+
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
82+
contentDescription = "Back",
83+
modifier = Modifier.clickable { onBackClick() }
84+
)
85+
Text(
86+
text = "Replies",
87+
style = MaterialTheme.typography.titleMedium,
88+
modifier = Modifier.padding(start = 16.dp)
89+
)
6890
}
6991
}
70-
}
92+
} else {
93+
item { Spacer(modifier = Modifier.height(8.dp)) }
94+
}
7195

72-
CompositionLocalProvider(LocalOverscrollFactory provides null) {
73-
LazyColumn(
74-
modifier = modifier.fillMaxSize(),
75-
state = listState
76-
) {
77-
if (isLoading && comments.isEmpty()) {
78-
item {
79-
Box(
80-
modifier = Modifier
81-
.fillMaxWidth()
82-
.padding(32.dp),
83-
contentAlignment = Alignment.Center
84-
) {
85-
CircularProgressIndicator()
86-
}
87-
}
88-
} else {
89-
if (showStickyHeader) {
90-
stickyHeader {
91-
Row(
92-
modifier = Modifier
93-
.fillMaxWidth()
94-
.background(MaterialTheme.colorScheme.surface)
95-
.padding(bottom = 16.dp, top = 8.dp),
96-
verticalAlignment = Alignment.CenterVertically,
97-
horizontalArrangement = Arrangement.Start
98-
) {
99-
Icon(
100-
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
101-
contentDescription = "Back",
102-
modifier = Modifier.clickable { onBackClick() }
103-
)
104-
Text(
105-
text = "Replies",
106-
style = MaterialTheme.typography.titleMedium,
107-
modifier = Modifier.padding(start = 16.dp)
108-
)
109-
}
96+
items(
97+
items = uniqueItems,
98+
key = { it.url!! }
99+
) { comment ->
100+
CommentItem(
101+
commentInfo = comment,
102+
onReplyButtonClick = { onShowReplies(comment) },
103+
onChannelAvatarClick = {
104+
comment.authorUrl?.let {
105+
navController.navigate(Screen.Channel.createRoute(it, comment.serviceId!!))
106+
SharedContext.sharedVideoDetailViewModel.showAsBottomPlayer()
110107
}
111-
} else {
112-
item { Spacer(modifier = Modifier.height(8.dp)) }
113-
}
114-
115-
items(
116-
items = uniqueItems,
117-
key = { it.url!! }
118-
) { comment ->
119-
CommentItem(
120-
commentInfo = comment,
121-
onReplyButtonClick = { onShowReplies(comment) },
122-
onChannelAvatarClick = {
123-
comment.authorUrl?.let {
124-
navController.navigate(Screen.Channel.createRoute(it, comment.serviceId!!))
125-
SharedContext.sharedVideoDetailViewModel.showAsBottomPlayer()
126-
}
127-
},
128-
onTimestampClick = onTimestampClick
129-
)
130-
Spacer(modifier = Modifier.height(20.dp))
131-
}
108+
},
109+
onTimestampClick = onTimestampClick,
110+
modifier = Modifier.padding(horizontal = 16.dp)
111+
)
112+
Spacer(modifier = Modifier.height(20.dp))
113+
}
132114

133-
if (isLoading && comments.isNotEmpty()) {
134-
item {
135-
Box(
136-
modifier = Modifier
137-
.fillMaxWidth()
138-
.padding(16.dp),
139-
contentAlignment = Alignment.Center
140-
) {
141-
CircularProgressIndicator()
142-
}
143-
}
115+
if (isLoading && comments.isNotEmpty()) {
116+
item {
117+
Box(
118+
modifier = Modifier
119+
.fillMaxWidth()
120+
.padding(16.dp),
121+
contentAlignment = Alignment.Center
122+
) {
123+
CircularProgressIndicator()
144124
}
145125
}
146126
}
147127
}
148-
}
128+
}

android/src/main/kotlin/project/pipepipe/app/ui/screens/videodetail/CommentSection.kt

Lines changed: 100 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
11
package project.pipepipe.app.ui.screens.videodetail
22

33
import androidx.activity.compose.BackHandler
4-
import androidx.compose.foundation.layout.Box
4+
import androidx.compose.foundation.ExperimentalFoundationApi
55
import androidx.compose.foundation.layout.fillMaxSize
6+
import androidx.compose.foundation.lazy.LazyColumn
67
import androidx.compose.foundation.lazy.LazyListState
7-
import androidx.compose.runtime.Composable
8-
import androidx.compose.runtime.DisposableEffect
9-
import androidx.compose.runtime.collectAsState
10-
import androidx.compose.runtime.getValue
11-
import androidx.compose.runtime.remember
12-
import androidx.compose.runtime.rememberCoroutineScope
8+
import androidx.compose.runtime.*
139
import androidx.compose.ui.Modifier
1410
import androidx.navigation.NavHostController
11+
import kotlinx.coroutines.flow.distinctUntilChanged
1512
import kotlinx.coroutines.launch
1613
import project.pipepipe.app.SharedContext
1714
import project.pipepipe.app.ui.component.ErrorComponent
18-
import project.pipepipe.app.ui.list.CommentList
15+
import project.pipepipe.app.ui.list.commentListContent
16+
import project.pipepipe.shared.infoitem.StreamInfo
1917

18+
@OptIn(ExperimentalFoundationApi::class)
2019
@Composable
2120
fun CommentSection(
22-
modifier: Modifier = Modifier,
23-
onTimestampClick: (Long) -> Unit,
21+
streamInfo: StreamInfo,
2422
navController: NavHostController,
23+
onPlayAudioClick: () -> Unit,
24+
onAddToPlaylistClick: () -> Unit,
25+
onTimestampClick: (Long) -> Unit,
26+
modifier: Modifier = Modifier
2527
) {
2628
val viewModel = SharedContext.sharedVideoDetailViewModel
2729
val uiState by viewModel.uiState.collectAsState()
@@ -58,24 +60,56 @@ fun CommentSection(
5860

5961
val serviceId = uiState.currentStreamInfo?.serviceId
6062

61-
Box(modifier = modifier.fillMaxSize()) {
62-
if (commentsState.common.error != null) {
63-
ErrorComponent(
64-
error = commentsState.common.error!!,
65-
onRetry = {
66-
uiState.currentStreamInfo?.let { streamInfo ->
67-
coroutineScope.launch {
68-
viewModel.loadComments(streamInfo)
69-
}
63+
if (commentsState.common.error != null) {
64+
ErrorComponent(
65+
error = commentsState.common.error!!,
66+
onRetry = {
67+
uiState.currentStreamInfo?.let { streamInfo ->
68+
coroutineScope.launch {
69+
viewModel.loadComments(streamInfo)
7070
}
71-
},
72-
modifier = Modifier.fillMaxSize(),
73-
shouldStartFromTop = true
74-
)
75-
} else {
76-
when {
77-
commentsState.parentComment != null -> {
78-
CommentList(
71+
}
72+
},
73+
modifier = modifier.fillMaxSize(),
74+
shouldStartFromTop = true
75+
)
76+
} else {
77+
when {
78+
commentsState.parentComment != null -> {
79+
// Replies view with common header
80+
LaunchedEffect(repliesListState, commentsState.replies.nextPageUrl, commentsState.common.isLoading) {
81+
snapshotFlow {
82+
val layoutInfo = repliesListState.layoutInfo
83+
val lastVisibleIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
84+
val totalItemsCount = layoutInfo.totalItemsCount
85+
lastVisibleIndex to totalItemsCount
86+
}
87+
.distinctUntilChanged()
88+
.collect { (lastVisibleIndex, totalItemsCount) ->
89+
val isAtBottom = totalItemsCount > 0 && lastVisibleIndex == totalItemsCount - 1
90+
val hasMoreReplies = commentsState.replies.nextPageUrl != null
91+
if (isAtBottom && hasMoreReplies && !commentsState.common.isLoading) {
92+
serviceId?.let {
93+
viewModel.loadMoreReplies(it)
94+
}
95+
}
96+
}
97+
}
98+
99+
LazyColumn(
100+
modifier = modifier.fillMaxSize(),
101+
state = repliesListState
102+
) {
103+
// Common header
104+
videoDetailCommonHeader(
105+
streamInfo = streamInfo,
106+
navController = navController,
107+
onPlayAudioClick = onPlayAudioClick,
108+
onAddToPlaylistClick = onAddToPlaylistClick
109+
)
110+
111+
// Replies content (inline from CommentList)
112+
commentListContent(
79113
comments = commentsState.replies.itemList,
80114
isLoading = commentsState.common.isLoading,
81115
hasMoreComments = commentsState.replies.nextPageUrl != null,
@@ -89,16 +123,49 @@ fun CommentSection(
89123
},
90124
showStickyHeader = true,
91125
onBackClick = { viewModel.backToCommentList() },
92-
listState = repliesListState,
93126
navController = navController,
94127
onTimestampClick = onTimestampClick
95128
)
96-
BackHandler() {
97-
viewModel.backToCommentList()
129+
}
130+
BackHandler {
131+
viewModel.backToCommentList()
132+
}
133+
}
134+
else -> {
135+
// Comments list with common header
136+
LaunchedEffect(commentsListState, commentsState.comments.nextPageUrl, commentsState.common.isLoading) {
137+
snapshotFlow {
138+
val layoutInfo = commentsListState.layoutInfo
139+
val lastVisibleIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
140+
val totalItemsCount = layoutInfo.totalItemsCount
141+
lastVisibleIndex to totalItemsCount
98142
}
143+
.distinctUntilChanged()
144+
.collect { (lastVisibleIndex, totalItemsCount) ->
145+
val isAtBottom = totalItemsCount > 0 && lastVisibleIndex == totalItemsCount - 1
146+
val hasMoreComments = commentsState.comments.nextPageUrl != null
147+
if (isAtBottom && hasMoreComments && !commentsState.common.isLoading) {
148+
serviceId?.let {
149+
viewModel.loadMoreComments(it)
150+
}
151+
}
152+
}
99153
}
100-
else -> {
101-
CommentList(
154+
155+
LazyColumn(
156+
modifier = modifier.fillMaxSize(),
157+
state = commentsListState
158+
) {
159+
// Common header
160+
videoDetailCommonHeader(
161+
streamInfo = streamInfo,
162+
navController = navController,
163+
onPlayAudioClick = onPlayAudioClick,
164+
onAddToPlaylistClick = onAddToPlaylistClick
165+
)
166+
167+
// Comments content (inline from CommentList)
168+
commentListContent(
102169
comments = commentsState.comments.itemList,
103170
isLoading = commentsState.common.isLoading,
104171
hasMoreComments = commentsState.comments.nextPageUrl != null,
@@ -116,8 +183,8 @@ fun CommentSection(
116183
}
117184
}
118185
},
186+
showStickyHeader = false,
119187
onBackClick = { },
120-
listState = commentsListState,
121188
navController = navController,
122189
onTimestampClick = onTimestampClick
123190
)

0 commit comments

Comments
 (0)