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-systemuicontroller:$accompanist_version"
implementation "com.google.accompanist:accompanist-permissions:$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-animation:$accompanist_version"
implementation "com.google.accompanist:accompanist-navigation-material:$accompanist_version"
implementation "com.google.accompanist:accompanist-flowlayout:$accompanist_version" implementation "com.google.accompanist:accompanist-flowlayout:$accompanist_version"
// KTOR - HTTP+WebSocket Library // KTOR - HTTP+WebSocket Library

View File

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