Skip to content
Snippets Groups Projects
Verified Commit 43828fd3 authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

Demo: Allow jumping to arbitrary points in the chat history, and load

properly in both directions
parent b9b43664
No related branches found
No related tags found
1 merge request!2Draft: Jetpack compose rewrite
Pipeline #564 passed
......@@ -29,6 +29,7 @@ import de.kuschku.libquassel.util.compatibility.LoggingHandler.LogLevel.DEBUG
import de.kuschku.libquassel.util.helper.value
import de.kuschku.quasseldroid.persistence.dao.findById
import de.kuschku.quasseldroid.persistence.dao.findFirstByBufferId
import de.kuschku.quasseldroid.persistence.dao.findLastByBufferId
import de.kuschku.quasseldroid.persistence.dao.get
import de.kuschku.quasseldroid.persistence.db.AccountDatabase
import de.kuschku.quasseldroid.persistence.db.QuasselDatabase
......@@ -41,13 +42,13 @@ class BacklogRequester(
private val database: QuasselDatabase,
private val accountDatabase: AccountDatabase
) {
fun loadMore(accountId: AccountId, buffer: BufferId, amount: Int, pageSize: Int,
fun loadMoreBefore(accountId: AccountId, buffer: BufferId, amount: Int, pageSize: Int,
lastMessageId: MsgId? = null,
untilAllVisible: Boolean = false,
finishCallback: () -> Unit) {
log(DEBUG,
"BacklogRequester",
"requested(bufferId: $buffer, amount: $amount, pageSize: $pageSize, lastMessageId: $lastMessageId, untilAllVisible: $untilAllVisible)")
"requestedBefore(bufferId: $buffer, amount: $amount, pageSize: $pageSize, lastMessageId: $lastMessageId, untilAllVisible: $untilAllVisible)")
var missing = amount
session.value?.orNull()?.let { session: ISession ->
session.backlogManager.let {
......@@ -56,14 +57,26 @@ class BacklogRequester(
buffer,
accountDatabase.accounts().findById(accountId)?.defaultFiltered ?: 0
)
val msgId = lastMessageId
?: database.message().findFirstByBufferId(buffer)?.messageId
?: MsgId(-1)
log(DEBUG,
"BacklogRequester",
"requestBefore(bufferId: $buffer, first: -1, last: $msgId, limit: $amount)")
it.requestBacklog(
bufferId = buffer,
last = lastMessageId
?: database.message().findFirstByBufferId(buffer)?.messageId
?: MsgId(-1),
last = msgId,
limit = amount
) {
if (it.isNotEmpty()) {
val min = it.asSequence().map(Message::messageId).min()
val max = it.asSequence().map(Message::messageId).max()
log(DEBUG,
"BacklogRequester",
"receivedBefore(bufferId: $buffer, [$min-$max])")
if (min == max) {
// Do not change stored messages if we only got back one message
false
} else if (it.isNotEmpty()) {
missing -= it.count {
(it.type.value and filtered.toUInt().inv()) != 0u &&
!QuasselBacklogStorage.isIgnored(session, it)
......@@ -72,7 +85,70 @@ class BacklogRequester(
val hasLoadedAny = missing < amount
if (untilAllVisible && !hasLoadedAll || !untilAllVisible && !hasLoadedAny) {
val messageId = it.map(Message::messageId).min()
loadMore(accountId,
loadMoreBefore(accountId,
buffer,
missing,
pageSize,
messageId,
untilAllVisible,
finishCallback)
true
} else {
finishCallback()
true
}
} else {
finishCallback()
true
}
}
}
}
}
fun loadMoreAfter(accountId: AccountId, buffer: BufferId, amount: Int, pageSize: Int,
lastMessageId: MsgId? = null,
untilAllVisible: Boolean = false,
finishCallback: () -> Unit) {
log(DEBUG,
"BacklogRequester",
"loadMoreAfter(bufferId: $buffer, amount: $amount, pageSize: $pageSize, lastMessageId: $lastMessageId, untilAllVisible: $untilAllVisible)")
var missing = amount
session.value?.orNull()?.let { session: ISession ->
session.backlogManager.let {
val filtered = database.filtered().get(
accountId,
buffer,
accountDatabase.accounts().findById(accountId)?.defaultFiltered ?: 0
)
val msgId = lastMessageId
?: database.message().findLastByBufferId(buffer)?.messageId
?: MsgId(0)
log(DEBUG,
"BacklogRequester",
"requestAfter(bufferId: $buffer, first: $msgId, last: -1, limit: $amount)")
it.requestBacklogForward(
bufferId = buffer,
first = msgId,
limit = amount
) {
val min = it.asSequence().map(Message::messageId).min()
val max = it.asSequence().map(Message::messageId).max()
log(DEBUG,
"BacklogRequester",
"receivedAfter(bufferId: $buffer, [$min-$max])")
if (min == max) {
// Do not change stored messages if we only got back one message
false
} else if (it.isNotEmpty()) {
missing -= it.count {
(it.type.value and filtered.toUInt().inv()) != 0u &&
!QuasselBacklogStorage.isIgnored(session, it)
}
val hasLoadedAll = missing == 0
val hasLoadedAny = missing < amount
if (untilAllVisible && !hasLoadedAll || !untilAllVisible && !hasLoadedAny) {
val messageId = it.map(Message::messageId).max()
loadMoreAfter(accountId,
buffer,
missing,
pageSize,
......
......@@ -45,6 +45,8 @@ import de.kuschku.libquassel.protocol.*
import de.kuschku.libquassel.quassel.BufferInfo
import de.kuschku.libquassel.quassel.syncables.BufferSyncer
import de.kuschku.libquassel.session.SessionManager
import de.kuschku.libquassel.util.compatibility.LoggingHandler.Companion.log
import de.kuschku.libquassel.util.compatibility.LoggingHandler.LogLevel.DEBUG
import de.kuschku.libquassel.util.flag.hasFlag
import de.kuschku.libquassel.util.helper.*
import de.kuschku.libquassel.util.irc.HostmaskHelper
......@@ -77,6 +79,9 @@ class MessageListFragment : ServiceBoundFragment() {
@BindView(R.id.messages)
lateinit var messageList: RecyclerView
@BindView(R.id.scrollUp)
lateinit var scrollUp: FloatingActionButton
@BindView(R.id.scrollDown)
lateinit var scrollDown: FloatingActionButton
......@@ -119,7 +124,8 @@ class MessageListFragment : ServiceBoundFragment() {
private var lastBuffer: BufferId? = null
private var previousMessageId: MsgId? = null
private var previousLoadKey: Int? = null
private var previousLoadKey: MsgId_Type? = null
private var reverse: Boolean = false
private var actionMode: ActionMode? = null
......@@ -230,9 +236,15 @@ class MessageListFragment : ServiceBoundFragment() {
}
private val boundaryCallback = object : PagedList.BoundaryCallback<DisplayMessage>() {
override fun onItemAtFrontLoaded(itemAtFront: DisplayMessage) = Unit
override fun onItemAtFrontLoaded(itemAtFront: DisplayMessage) {
val id = itemAtFront.tag.id
log(DEBUG, "MessageListFragment", "onItemAtFrontLoaded: $id")
loadMore(reverse = true, lastMessageId = id)
}
override fun onItemAtEndLoaded(itemAtEnd: DisplayMessage) {
loadMore()
val id = itemAtEnd.tag.id
log(DEBUG, "MessageListFragment", "onItemAtEndLoaded: $id")
loadMore(reverse = false, lastMessageId = id)
}
}
......@@ -327,6 +339,11 @@ class MessageListFragment : ServiceBoundFragment() {
val isScrollingDown = dy > 0
scrollDown.toggle(canScrollDown && isScrollingDown)
val canScrollUp = recyclerView.canScrollVertically(-1)
val isScrollingUp = dy < 0
scrollUp.toggle(canScrollUp && isScrollingUp)
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
......@@ -396,7 +413,7 @@ class MessageListFragment : ServiceBoundFragment() {
.setEnablePlaceholders(true)
.build()
).setBoundaryCallback(boundaryCallback)
.setInitialLoadKey(previousLoadKey)
.setInitialLoadKey(previousLoadKey?.toInt())
.build()
}
}
......@@ -461,7 +478,7 @@ class MessageListFragment : ServiceBoundFragment() {
var hasLoaded = false
fun checkScroll() {
if (hasLoaded) {
if (linearLayoutManager.findFirstVisibleItemPosition() < 2 && !isScrolling) {
if (!reverse && linearLayoutManager.findFirstVisibleItemPosition() < 2 && !isScrolling) {
messageList.scrollToPosition(0)
}
} else {
......@@ -483,7 +500,12 @@ class MessageListFragment : ServiceBoundFragment() {
(fab as View).visibility = View.VISIBLE
}
})
scrollDown.setOnClickListener { messageList.scrollToPosition(0) }
scrollDown.setOnClickListener {
jumpTo(false)
}
scrollUp.setOnClickListener {
jumpTo(true)
}
val avatarSize = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
......@@ -512,12 +534,12 @@ class MessageListFragment : ServiceBoundFragment() {
savedInstanceState?.run {
(messageList.layoutManager as RecyclerView.LayoutManager).onRestoreInstanceState(getParcelable(
KEY_STATE_LIST))
previousLoadKey = getInt(KEY_STATE_PAGING).nullIf { it == -1 }
previousLoadKey = getLong(KEY_STATE_PAGING).nullIf { it == -1L }
lastBuffer = BufferId(getInt(KEY_STATE_BUFFER)).nullIf { !it.isValidId() }
}
data.observe(viewLifecycleOwner, Observer { list ->
previousLoadKey = list?.lastKey as? Int
previousLoadKey = (list?.lastKey as? Int)?.toLong()
val firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition()
val firstVisibleMessageId = adapter[firstVisibleItemPosition]?.content?.messageId
runInBackground {
......@@ -534,6 +556,7 @@ class MessageListFragment : ServiceBoundFragment() {
type = Buffer_Type.of(Buffer_Type.StatusBuffer)
)?.bufferId ?: BufferId(0)
if (buffer != lastBuffer) {
reverse = false
adapter.clearCache()
modelHelper.connectedSession.value?.orNull()?.bufferSyncer?.let { bufferSyncer ->
onBufferChange(lastBuffer,
......@@ -552,10 +575,26 @@ class MessageListFragment : ServiceBoundFragment() {
return view
}
private fun jumpTo(start: Boolean) {
reverse = start
runInBackground {
modelHelper.chat.bufferId { bufferId ->
//if (database.message().find(msg.id) == null) {
database.message().clearMessages(bufferId.id)
//}
if (start) {
loadMore(initial = true, reverse = true, lastMessageId = MsgId(0L))
} else {
loadMore(initial = true, reverse = false, lastMessageId = MsgId(-1L))
}
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(KEY_STATE_LIST, messageList.layoutManager?.onSaveInstanceState())
outState.putInt(KEY_STATE_PAGING, previousLoadKey ?: -1)
outState.putLong(KEY_STATE_PAGING, previousLoadKey ?: -1L)
outState.putInt(KEY_STATE_BUFFER, lastBuffer?.id ?: -1)
}
......@@ -607,14 +646,30 @@ class MessageListFragment : ServiceBoundFragment() {
super.onPause()
}
private fun loadMore(initial: Boolean = false, lastMessageId: MsgId? = null) {
private fun loadMore(initial: Boolean = false, lastMessageId: MsgId? = null, reverse: Boolean = this.reverse) {
// This can be called *after* we’re already detached from the activity
activity?.runOnUiThread {
modelHelper.chat.bufferId { bufferId ->
if (bufferId.isValidId() && bufferId != BufferId.MAX_VALUE) {
if (initial) swipeRefreshLayout.isRefreshing = true
runInBackground {
backlogRequester.loadMore(
if (reverse) {
backlogRequester.loadMoreAfter(
accountId = accountId,
buffer = bufferId,
amount = if (initial) backlogSettings.initialAmount else backlogSettings.pageSize,
pageSize = backlogSettings.pageSize,
lastMessageId = lastMessageId
?: database.message().findLastByBufferId(bufferId)?.messageId
?: MsgId(0),
untilAllVisible = initial
) {
activity?.runOnUiThread {
swipeRefreshLayout.isRefreshing = false
}
}
} else {
backlogRequester.loadMoreBefore(
accountId = accountId,
buffer = bufferId,
amount = if (initial) backlogSettings.initialAmount else backlogSettings.pageSize,
......@@ -633,6 +688,7 @@ class MessageListFragment : ServiceBoundFragment() {
}
}
}
}
companion object {
private const val KEY_STATE_LIST = "KEY_STATE_LIST"
......
<!--
Quasseldroid - Quassel client for Android
Copyright (c) 2020 Janne Mareike Koschinski
Copyright (c) 2020 The Quassel Project
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3 as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M7.41,18.41L6,17L12,11L18,17L16.59,18.41L12,13.83L7.41,18.41M7.41,12.41L6,11L12,5L18,11L16.59,12.41L12,7.83L7.41,12.41Z" />
</vector>
......@@ -39,14 +39,30 @@
tools:listitem="@layout/widget_chatmessage_plain" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="end|bottom"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/scrollUp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="@color/colorFillDark"
android:visibility="gone"
app:backgroundTint="#8A808080"
app:elevation="0dip"
app:fabSize="mini"
app:pressedTranslationZ="0dip"
app:srcCompat="@drawable/ic_scroll_up" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/scrollDown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:tint="@color/colorFillDark"
android:visibility="gone"
app:backgroundTint="#8A808080"
......@@ -55,4 +71,6 @@
app:pressedTranslationZ="0dip"
app:srcCompat="@drawable/ic_scroll_down" />
</LinearLayout>
</FrameLayout>
......@@ -32,6 +32,7 @@ class BacklogManager(
) : SyncableObject(session.proxy, "BacklogManager"), IBacklogManager {
private val loading = mutableMapOf<BufferId, (List<Message>) -> Boolean>()
private val loadingFiltered = mutableMapOf<BufferId, (List<Message>) -> Boolean>()
private val loadingForward = mutableMapOf<BufferId, (List<Message>) -> Boolean>()
override fun deinit() {
super.deinit()
......@@ -60,6 +61,15 @@ class BacklogManager(
requestBacklogFiltered(bufferId, first, last, limit, additional, type, flags)
}
fun requestBacklogForward(bufferId: BufferId, first: MsgId = MsgId(-1),
last: MsgId = MsgId(-1), limit: Int = -1,
type: Int = 0, flags: Int = 0,
callback: (List<Message>) -> Boolean) {
if (loadingForward.contains(bufferId)) return
loadingForward[bufferId] = callback
requestBacklogForward(bufferId, first, last, limit, type, flags)
}
fun requestBacklogAll(first: MsgId = MsgId(-1), last: MsgId = MsgId(-1), limit: Int = -1,
additional: Int = 0, callback: (List<Message>) -> Boolean) {
if (loading.contains(BufferId(-1))) return
......@@ -103,6 +113,16 @@ class BacklogManager(
}
}
override fun receiveBacklogForward(bufferId: BufferId, first: MsgId, last: MsgId, limit: Int,
type: Int, flags: Int,
messages: QVariantList) {
val list = messages.mapNotNull<QVariant_, Message>(QVariant_::value)
if (loadingForward.remove(bufferId)?.invoke(list) != false) {
log(DEBUG, "BacklogManager", "storeMessages(${list.size})")
backlogStorage?.storeMessages(session, list)
}
}
override fun receiveBacklogAllFiltered(first: MsgId, last: MsgId, limit: Int, additional: Int,
type: Int, flags: Int, messages: QVariantList) {
val list = messages.mapNotNull<QVariant_, Message>(QVariant_::value)
......
......@@ -46,6 +46,17 @@ interface IBacklogManager : ISyncableObject {
)
}
@Slot
fun requestBacklogForward(bufferId: BufferId, first: MsgId = MsgId(-1),
last: MsgId = MsgId(-1), limit: Int = -1,
type: Int = -1, flags: Int = -1) {
REQUEST(
"requestBacklogForward", ARG(bufferId, QType.BufferId), ARG(first, QType.MsgId),
ARG(last, QType.MsgId), ARG(limit, Type.Int), ARG(type, Type.Int),
ARG(flags, Type.Int)
)
}
@Slot
fun requestBacklogAll(first: MsgId = MsgId(-1), last: MsgId = MsgId(-1), limit: Int = -1,
additional: Int = 0) {
......@@ -73,6 +84,10 @@ interface IBacklogManager : ISyncableObject {
fun receiveBacklogFiltered(bufferId: BufferId, first: MsgId, last: MsgId, limit: Int,
additional: Int, type: Int, flags: Int, messages: QVariantList)
@Slot
fun receiveBacklogForward(bufferId: BufferId, first: MsgId, last: MsgId, limit: Int,
type: Int, flags: Int, messages: QVariantList)
@Slot
fun receiveBacklogAll(first: MsgId, last: MsgId, limit: Int, additional: Int,
messages: QVariantList)
......
......@@ -48,6 +48,8 @@ open class ChatViewModel : QuasselViewModel() {
val stateReset = BehaviorSubject.create<Unit>()
val bufferOpened = PublishSubject.create<Unit>()
val loadKey = BehaviorSubject.create<MsgId>()
fun onSaveInstanceState(outState: Bundle) {
/*
outState.putSerializable(
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment