diff --git a/app/src/main/java/de/kuschku/quasseldroid/service/BacklogRequester.kt b/app/src/main/java/de/kuschku/quasseldroid/service/BacklogRequester.kt index 38215a15d45b7ce83e0a6f1ec96325e14199387e..ae197b527ad4b704ea0106590bbc3257b8f67cbd 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/service/BacklogRequester.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/service/BacklogRequester.kt @@ -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, - lastMessageId: MsgId? = null, - untilAllVisible: Boolean = false, - finishCallback: () -> Unit) { + 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,7 @@ 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, @@ -92,4 +105,67 @@ class BacklogRequester( } } } + 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, + messageId, + untilAllVisible, + finishCallback) + true + } else { + finishCallback() + true + } + } else { + finishCallback() + true + } + } + } + } + } } diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt index 99fe0be42caddb5633968b6caab41a8298694f8e..c878d8ef2f39637f3c0c953cdf826287718cbc43 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt @@ -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,25 +646,42 @@ 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( - accountId = accountId, - buffer = bufferId, - amount = if (initial) backlogSettings.initialAmount else backlogSettings.pageSize, - pageSize = backlogSettings.pageSize, - lastMessageId = lastMessageId - ?: database.message().findFirstByBufferId(bufferId)?.messageId - ?: MsgId(-1), - untilAllVisible = initial - ) { - activity?.runOnUiThread { - swipeRefreshLayout.isRefreshing = false + 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, + pageSize = backlogSettings.pageSize, + lastMessageId = lastMessageId + ?: database.message().findFirstByBufferId(bufferId)?.messageId + ?: MsgId(-1), + untilAllVisible = initial + ) { + activity?.runOnUiThread { + swipeRefreshLayout.isRefreshing = false + } } } } diff --git a/app/src/main/res/drawable/ic_scroll_up.xml b/app/src/main/res/drawable/ic_scroll_up.xml new file mode 100644 index 0000000000000000000000000000000000000000..b7717298a4d08022cb489935afed176cfe1ebae6 --- /dev/null +++ b/app/src/main/res/drawable/ic_scroll_up.xml @@ -0,0 +1,28 @@ +<!-- + 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> diff --git a/app/src/main/res/layout/chat_messages.xml b/app/src/main/res/layout/chat_messages.xml index 3ee860ba44739065696a7054dc46d008f3aae25f..148eaf96457830ce5f4d30404f07d68cb75b96d0 100644 --- a/app/src/main/res/layout/chat_messages.xml +++ b/app/src/main/res/layout/chat_messages.xml @@ -39,20 +39,38 @@ tools:listitem="@layout/widget_chatmessage_plain" /> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> - - <com.google.android.material.floatingactionbutton.FloatingActionButton - android:id="@+id/scrollDown" + <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" - android:tint="@color/colorFillDark" - android:visibility="gone" - app:backgroundTint="#8A808080" - app:elevation="0dip" - app:fabSize="mini" - app:pressedTranslationZ="0dip" - app:srcCompat="@drawable/ic_scroll_down" /> + 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:tint="@color/colorFillDark" + android:visibility="gone" + app:backgroundTint="#8A808080" + app:elevation="0dip" + app:fabSize="mini" + app:pressedTranslationZ="0dip" + app:srcCompat="@drawable/ic_scroll_down" /> + + </LinearLayout> </FrameLayout> diff --git a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/BacklogManager.kt b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/BacklogManager.kt index f119d9ae40594a6ac8971504ef2611cfb6d60d2c..1ddef1fc09fa2d4818dda29a45322977b4a15953 100644 --- a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/BacklogManager.kt +++ b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/BacklogManager.kt @@ -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) diff --git a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/interfaces/IBacklogManager.kt b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/interfaces/IBacklogManager.kt index 4ab656275e5609dae90a1da774d0900bf54c6a6a..23f1f0898631b57fd8f65f3b6ab16c3366cc0e25 100644 --- a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/interfaces/IBacklogManager.kt +++ b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/interfaces/IBacklogManager.kt @@ -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) diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/ChatViewModel.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/ChatViewModel.kt index 2004ee963fc95c1a566845d64a4cf2cad59565f1..dd800dfad356e319368b53b631c4f96770f5c6f5 100644 --- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/ChatViewModel.kt +++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/ChatViewModel.kt @@ -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(