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:
parent
cc2b2c95a3
commit
c6145046d3
3 changed files with 188 additions and 26 deletions
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue