feat: infinite scroll in channels

This commit is contained in:
Infi 2023-02-05 20:46:05 +01:00
parent 9017fc52a5
commit 493a542ae9
2 changed files with 54 additions and 134 deletions

View File

@ -80,6 +80,7 @@ dependencies {
implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_version"
implementation "com.google.accompanist:accompanist-permissions:$accompanist_version"
implementation "com.google.accompanist:accompanist-navigation-animation:$accompanist_version"
implementation "com.google.accompanist:accompanist-navigation-material:$accompanist_version"
implementation "com.google.accompanist:accompanist-flowlayout:$accompanist_version"
// KTOR - HTTP+WebSocket Library

View File

@ -1,7 +1,6 @@
package chat.revolt.screens.chat.views
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.*
@ -17,17 +16,11 @@ import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -52,12 +45,11 @@ import chat.revolt.api.routes.user.addUserIfUnknown
import chat.revolt.api.schemas.Channel
import chat.revolt.components.chat.Message
import chat.revolt.components.chat.MessageField
import chat.revolt.components.generic.CollapsibleCard
import chat.revolt.components.generic.PageHeader
import chat.revolt.components.screens.chat.AttachmentManager
import chat.revolt.components.screens.chat.ChannelIcon
import chat.revolt.components.screens.chat.TypingIndicator
import io.ktor.http.*
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import kotlinx.datetime.Instant
import java.io.File
@ -187,20 +179,34 @@ class ChannelScreenViewModel : ViewModel() {
viewModelScope.launch {
val messages = arrayListOf<MessageSchema>()
fetchMessagesFromChannel(
channel!!.id!!,
limit = 50,
true,
before = renderableMessages.last().id
).let {
it.messages!!.forEach { message ->
addUserIfUnknown(message.author ?: return@forEach)
if (!RevoltAPI.messageCache.containsKey(message.id)) {
RevoltAPI.messageCache[message.id!!] = message
if (!renderableMessages.isEmpty()) {
fetchMessagesFromChannel(
channel!!.id!!,
limit = 50,
true,
before = renderableMessages.last().id
).let {
it.messages!!.forEach { message ->
addUserIfUnknown(message.author ?: return@forEach)
if (!RevoltAPI.messageCache.containsKey(message.id)) {
RevoltAPI.messageCache[message.id!!] = message
}
messages.add(message)
}
}
} else {
fetchMessagesFromChannel(channel!!.id!!, limit = 50, true).let {
it.messages!!.forEach { message ->
addUserIfUnknown(message.author ?: return@forEach)
if (!RevoltAPI.messageCache.containsKey(message.id)) {
RevoltAPI.messageCache[message.id!!] = message
}
messages.add(message)
}
messages.add(message)
}
}
regroupMessages(renderableMessages + messages)
}
}
@ -213,7 +219,6 @@ class ChannelScreenViewModel : ViewModel() {
}
registerCallback()
fetchMessages()
}
fun sendPendingMessage() {
@ -286,82 +291,6 @@ class ChannelScreenViewModel : ViewModel() {
}
}
@Composable
fun ChannelInfoScreen(
channel: Channel,
viewModel: ChannelScreenViewModel,
onClosed: () -> Unit,
) {
val context = LocalContext.current
val clipboardManager: ClipboardManager =
LocalClipboardManager.current
val coroutineScope = rememberCoroutineScope()
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.padding(16.dp)
.fillMaxSize()
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
ChannelIcon(
channelType = channel.channelType!!,
modifier = Modifier.size(32.dp)
)
PageHeader(
text = channel.name ?: channel.id!!,
modifier = Modifier.offset((-8).dp, 0.dp)
)
}
Column(modifier = Modifier.weight(1f)) {
CollapsibleCard(title = "Advanced") {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text("Channel ID: ${channel.id}")
Button(onClick = {
clipboardManager.setText(AnnotatedString(channel.id!!))
Toast.makeText(
context,
"Copied",
Toast.LENGTH_SHORT
).show()
}) {
Text("Copy ID")
}
Button(
onClick = {
coroutineScope.launch {
viewModel.fetchMessages()
}
},
modifier = Modifier
.fillMaxWidth()
) {
Text("Refetch messages")
}
}
}
}
Button(
onClick = onClosed,
modifier = Modifier
.fillMaxWidth()
) {
Text("Close")
}
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun ChannelScreen(
navController: NavController,
@ -374,8 +303,6 @@ fun ChannelScreen(
val lazyListState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
val channelInfoOpen = remember { mutableStateOf(false) }
val pickFileLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenMultipleDocuments()
) { uriList ->
@ -421,28 +348,11 @@ fun ChannelScreen(
return
}
if (channelInfoOpen.value) {
Dialog(
onDismissRequest = {
channelInfoOpen.value = false
},
properties = DialogProperties(
usePlatformDefaultWidth = false,
)
) {
ChannelInfoScreen(channel, viewModel) {
channelInfoOpen.value = false
}
}
}
Column {
Row(
modifier = Modifier
.clickable {
coroutineScope.launch {
channelInfoOpen.value = true
}
navController.navigate("channel/${channel.id}/info")
}
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
@ -461,21 +371,36 @@ fun ChannelScreen(
)
}
val isScrolledToBottom = remember(lazyListState) {
derivedStateOf {
lazyListState.firstVisibleItemIndex <= 6
}
}
LaunchedEffect(viewModel.renderableMessages.size) {
if (isScrolledToBottom.value) {
coroutineScope.launch {
lazyListState.animateScrollToItem(0)
}
val isScrolledToTop = remember {
derivedStateOf {
val layoutInfo = lazyListState.layoutInfo
val totalItemsNumber = layoutInfo.totalItemsCount
val lastVisibleItemIndex =
(layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1
val buffer = if (totalItemsNumber > 6) 6 else 0
lastVisibleItemIndex > (totalItemsNumber - buffer)
}
}
LaunchedEffect(isScrolledToTop) {
snapshotFlow { isScrolledToTop.value }
.distinctUntilChanged()
.collect {
if (it) {
coroutineScope.launch {
viewModel.fetchOlderMessages()
}
}
}
}
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.BottomEnd
@ -490,17 +415,11 @@ fun ChannelScreen(
}
item {
Button(
onClick = {
coroutineScope.launch {
viewModel.fetchOlderMessages()
}
},
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp, horizontal = 8.dp)
) {
Text("Load older")
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(
modifier = Modifier
.padding(16.dp)
)
}
}
}