1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-3298 Chats | Enhancement | Scroll to unread-messages section (#2375)

This commit is contained in:
Evgenii Kozlov 2025-05-05 16:03:31 +02:00 committed by GitHub
parent cc2b2c95a3
commit c6145046d3
Signed by: github
GPG key ID: B5690EEEBB952194
3 changed files with 188 additions and 26 deletions

View file

@ -169,7 +169,10 @@ class ChatContainer @Inject constructor(
}
is Transformation.Commands.LoadAround -> {
val messages = try {
loadAroundMessage(chat, transform.message)
loadAroundMessage(
chat = chat,
msg = transform.message
)
} catch (e: Exception) {
logger.logException(e, "DROID-2966 Error while loading reply context")
state.messages
@ -182,26 +185,78 @@ class ChatContainer @Inject constructor(
}
is Transformation.Commands.LoadEnd -> {
if (state.messages.isNotEmpty()) {
val lastShown = state.messages.last()
val lastTracked = lastMessages.entries.first().value
if (lastShown.id == lastTracked.id) {
// No need to paginate, just scroll to bottom.
state.copy(
intent = Intent.ScrollToBottom
)
} else {
val messages = try {
loadToEnd(chat)
} catch (e: Exception) {
state.messages.also {
logger.logException(e, "DROID-2966 Error while scrolling to bottom")
}
if (state.state.hasUnReadMessages) {
// Check if above the unread messages
val oldestReadOrderId = state.state.oldestMessageOrderId
val bottomMessage = state.messages.find {
it.id == transform.lastVisibleMessage
}
if (bottomMessage != null && oldestReadOrderId != null) {
if (bottomMessage.order < oldestReadOrderId) {
// Scroll to the first unread message
val messages = try {
loadAroundMessageOrder(
chat = chat,
order = oldestReadOrderId
)
} catch (e: Exception) {
logger.logException(e, "DROID-2966 Error while loading reply context")
state.messages
}
val target = messages.find { it.order == oldestReadOrderId }
ChatStreamState(
messages = messages,
intent = if (target != null) Intent.ScrollToMessage(target.id) else Intent.ScrollToBottom,
state = state.state
)
} else {
val messages = try {
loadToEnd(chat)
} catch (e: Exception) {
state.messages.also {
logger.logException(e, "DROID-2966 Error while scrolling to bottom")
}
}
ChatStreamState(
messages = messages,
intent = Intent.ScrollToBottom,
state = state.state
)
}
} else {
val messages = try {
loadToEnd(chat)
} catch (e: Exception) {
state.messages.also {
logger.logException(e, "DROID-2966 Error while scrolling to bottom")
}
}
ChatStreamState(
messages = messages,
intent = Intent.ScrollToBottom,
state = state.state
)
}
} else {
if (lastMessages.contains(transform.lastVisibleMessage)) {
// No need to paginate, just scroll to bottom.
state.copy(
intent = Intent.ScrollToBottom
)
} else {
val messages = try {
loadToEnd(chat)
} catch (e: Exception) {
state.messages.also {
logger.logException(e, "DROID-2966 Error while scrolling to bottom")
}
}
ChatStreamState(
messages = messages,
intent = Intent.ScrollToBottom,
state = state.state
)
}
ChatStreamState(
messages = messages,
intent = Intent.ScrollToBottom,
state = state.state
)
}
} else {
state
@ -477,9 +532,9 @@ class ChatContainer @Inject constructor(
commands.emit(Transformation.Commands.LoadAround(message = replyMessage))
}
suspend fun onLoadChatTail() {
suspend fun onLoadChatTail(msg: Id?) {
logger.logInfo("DROID-2966 emitting onLoadEnd")
commands.emit(Transformation.Commands.LoadEnd)
commands.emit(Transformation.Commands.LoadEnd(msg))
}
suspend fun onVisibleRangeChanged(from: Id, to: Id) {
@ -527,7 +582,7 @@ class ChatContainer @Inject constructor(
/**
* Scroll-to-bottom behavior.
*/
data object LoadEnd: Commands()
data class LoadEnd(val lastVisibleMessage: Id?): Commands()
data class UpdateVisibleRange(val from: Id, val to: Id) : Commands()
}

View file

@ -15,6 +15,7 @@ import com.anytypeio.anytype.test_utils.MockDataFactory
import kotlin.test.assertEquals
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
@ -391,6 +392,100 @@ class ChatContainerTest {
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `should scroll to the first unread message when scroll-to-bottom is clicked when subscribing chat`() = runTest {
val container = ChatContainer(
repo = repo,
channel = channel,
logger = logger
)
val messages = buildList {
repeat(100) {
add(
StubChatMessage(
id = it.toString(),
order = it.toString()
)
)
}
}
val state = Chat.State.UnreadState(
counter = 10,
olderOrderId = "90"
)
repo.stub {
onBlocking {
subscribeLastChatMessages(
Command.ChatCommand.SubscribeLastMessages(
chat = givenChatID,
limit = ChatContainer.DEFAULT_CHAT_PAGING_SIZE
)
)
} doReturn Command.ChatCommand.SubscribeLastMessages.Response(
messages = messages.takeLast(10),
messageCountBefore = 0,
chatState = Chat.State(unreadMessages = state)
)
onBlocking {
getChatMessages(
Command.ChatCommand.GetMessages(
chat = givenChatID,
beforeOrderId = state.olderOrderId,
limit = ChatContainer.DEFAULT_CHAT_PAGING_SIZE,
afterOrderId = null,
includeBoundary = false
)
)
} doReturn Command.ChatCommand.GetMessages.Response(
messages = messages.slice(80..89)
)
onBlocking {
getChatMessages(
Command.ChatCommand.GetMessages(
chat = givenChatID,
afterOrderId = state.olderOrderId,
limit = ChatContainer.DEFAULT_CHAT_PAGING_SIZE,
includeBoundary = true
)
)
} doReturn Command.ChatCommand.GetMessages.Response(
messages = messages.slice(90..99)
)
}
channel.stub {
on { observe(chat = givenChatID) } doReturn emptyFlow()
}
container.watch(givenChatID).test {
val initial = awaitItem()
assertEquals(
expected = messages.slice(80..89) + messages.slice(90..99),
actual = initial.messages,
)
assertEquals(
expected = ChatContainer.Intent.ScrollToMessage(id = "90"),
actual = initial.intent,
)
container.onLoadChatTail(
msg = "80"
)
// New state is not emitted, since it does not change.
}
}
// TODO move to test-utils
fun StubChatMessage(
id: Id = MockDataFactory.randomUuid(),

View file

@ -313,7 +313,7 @@ fun ChatScreen(
onChatScrolledToBottom: () -> Unit,
onScrollToReplyClicked: (Id) -> Unit,
onClearIntent: () -> Unit,
onScrollToBottomClicked: () -> Unit,
onScrollToBottomClicked: (Id?) -> Unit,
onVisibleRangeChanged: (Id, Id) -> Unit
) {
@ -382,7 +382,7 @@ fun ChatScreen(
.mapNotNull { item -> latestMessages.getOrNull(item.index) }
.filterIsInstance<ChatView.Message>()
if (visibleMessages.isNotEmpty()) {
if (visibleMessages.isNotEmpty() && !isPerformingScrollIntent.value) {
// TODO could be optimised by passing order ID
visibleMessages.first().id to visibleMessages.last().id
} else null
@ -468,7 +468,19 @@ fun ChatScreen(
.align(Alignment.BottomEnd)
.padding(end = 12.dp),
onGoToBottomClicked = {
onScrollToBottomClicked()
val lastVisibleIndex = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
val lastVisibleView = if (lastVisibleIndex != null) {
latestMessages.getOrNull(lastVisibleIndex)
} else {
null
}
if (lastVisibleView is ChatView.Message) {
onScrollToBottomClicked(
lastVisibleView.id
)
} else {
onScrollToBottomClicked(null)
}
},
enabled = jumpToBottomButtonEnabled
)