From 97b5a9ea898de5847498eeb3c0fff6e70674fd0b Mon Sep 17 00:00:00 2001 From: oakkitten Date: Fri, 15 Mar 2024 11:23:37 +0000 Subject: [PATCH 1/6] Split CircularImageButton into two classes In preparation for one of the subsequent commits. Don't reuse animations, as these are apparently mutable. Also, reduce hide animation duration by a factor of 2. --- ...rImageButton.kt => FloatingImageButton.kt} | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) rename app/src/main/java/com/ubergeek42/WeechatAndroid/views/{CircularImageButton.kt => FloatingImageButton.kt} (75%) diff --git a/app/src/main/java/com/ubergeek42/WeechatAndroid/views/CircularImageButton.kt b/app/src/main/java/com/ubergeek42/WeechatAndroid/views/FloatingImageButton.kt similarity index 75% rename from app/src/main/java/com/ubergeek42/WeechatAndroid/views/CircularImageButton.kt rename to app/src/main/java/com/ubergeek42/WeechatAndroid/views/FloatingImageButton.kt index d6e65bba..619675f5 100644 --- a/app/src/main/java/com/ubergeek42/WeechatAndroid/views/CircularImageButton.kt +++ b/app/src/main/java/com/ubergeek42/WeechatAndroid/views/FloatingImageButton.kt @@ -14,30 +14,48 @@ import com.ubergeek42.WeechatAndroid.upload.applicationContext import com.ubergeek42.WeechatAndroid.upload.f -class CircularImageButton @JvmOverloads constructor( +open class FloatingImageButton @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, ) : AppCompatImageButton(context, attrs) { - init { - background = null - outlineProvider = pillOutlineProvider - clipToOutline = true - } - - fun show() { + fun show(animate: Boolean = true) { if (visibility != VISIBLE) { visibility = VISIBLE - startAnimation(showAnimation) + if (animate) startAnimation(makeShowAnimation()) } } - fun hide() { + fun hide(animate: Boolean = true) { if (visibility != INVISIBLE) { visibility = INVISIBLE - startAnimation(hideAnimation) + if (animate) startAnimation(makeHideAnimation()) } } + private fun makeShowAnimation() = ScaleAnimation( + 0f, 1f, 0f, 1f, + Animation.RELATIVE_TO_SELF, 0.5f, + Animation.RELATIVE_TO_SELF, 0.5f) + .apply { duration = shortAnimTime } + + private fun makeHideAnimation() = ScaleAnimation( + 1f, .5f, 1f, 0.5f, + Animation.RELATIVE_TO_SELF, 0.5f, + Animation.RELATIVE_TO_SELF, 0.5f) + .apply { duration = shortAnimTime / 2 } +} + + +class CircularImageButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, +) : FloatingImageButton(context, attrs) { + init { + background = null + outlineProvider = pillOutlineProvider + clipToOutline = true + } + override fun setBackgroundColor(color: Int) { if (backgroundPaint.color != color) { backgroundPaint.color = color @@ -51,18 +69,6 @@ class CircularImageButton @JvmOverloads constructor( canvas.drawPaint(backgroundPaint) super.onDraw(canvas) } - - private val showAnimation = ScaleAnimation( - 0f, 1f, 0f, 1f, - Animation.RELATIVE_TO_SELF, 0.5f, - Animation.RELATIVE_TO_SELF, 0.5f) - .apply { duration = animationDuration } - - private val hideAnimation = ScaleAnimation( - 1f, .5f, 1f, 0.5f, - Animation.RELATIVE_TO_SELF, 0.5f, - Animation.RELATIVE_TO_SELF, 0.5f) - .apply { duration = animationDuration } } @@ -73,5 +79,5 @@ private val pillOutlineProvider = object : ViewOutlineProvider() { } -private val animationDuration = applicationContext +private val shortAnimTime = applicationContext .resources.getInteger(android.R.integer.config_shortAnimTime).toLong() From 677aabc43bc3d2199fe0a3612c551d87a704241a Mon Sep 17 00:00:00 2001 From: oakkitten Date: Fri, 15 Mar 2024 18:08:12 +0000 Subject: [PATCH 2/6] Tweak buffer list's hot & warm buffer badges a bit --- app/src/main/res/drawable/bg_bufferlist_item_hot.xml | 6 +++--- app/src/main/res/drawable/bg_bufferlist_item_warm.xml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/drawable/bg_bufferlist_item_hot.xml b/app/src/main/res/drawable/bg_bufferlist_item_hot.xml index d4ca78c4..7fe6e678 100644 --- a/app/src/main/res/drawable/bg_bufferlist_item_hot.xml +++ b/app/src/main/res/drawable/bg_bufferlist_item_hot.xml @@ -1,14 +1,14 @@ - + - + - + diff --git a/app/src/main/res/drawable/bg_bufferlist_item_warm.xml b/app/src/main/res/drawable/bg_bufferlist_item_warm.xml index e9667fdd..8f6caf13 100644 --- a/app/src/main/res/drawable/bg_bufferlist_item_warm.xml +++ b/app/src/main/res/drawable/bg_bufferlist_item_warm.xml @@ -1,14 +1,14 @@ - + - + - + From d54bfc4c5c2ce62dfa43246247d92a1debb048f6 Mon Sep 17 00:00:00 2001 From: oakkitten Date: Fri, 15 Mar 2024 18:21:02 +0000 Subject: [PATCH 3/6] Add off-screen hot buffer indicators in buffer list Indicators are located at the top and the bottom of the buffer list, touching window insents, if any. They are shown if there's a hot buffer above or below the visible buffers, including buffers that are partially visible. When pressed, they cause the buffer list to scroll to the next hot buffer in the direction of the button, trying to center the buffer. The indicators are image buttons with background that is transparent while the button is not pressed. Both the image and the background are rounded rectangles. The image is a combination of the badge that we use showing the number of highlights for a buffer in the buffer list, and the double arrow that we use for jumping to bottom in the chat. --- .../inkscape/ic_bufferlist_arrow_down.svg | 131 +++++++++++++++++ .../main/inkscape/ic_bufferlist_arrow_up.svg | 133 ++++++++++++++++++ .../optimized/ic_bufferlist_arrow_down.svg | 5 + .../optimized/ic_bufferlist_arrow_up.svg | 5 + .../adapters/BufferListAdapter.kt | 12 +- .../fragments/BufferListFragment.kt | 72 ++++++++-- .../views/FloatingImageButton.kt | 20 +++ .../views/FullScreenActivityController.kt | 6 + .../res/drawable/ic_bufferlist_arrow_down.xml | 9 ++ .../res/drawable/ic_bufferlist_arrow_up.xml | 9 ++ app/src/main/res/layout/bufferlist.xml | 20 +++ 11 files changed, 408 insertions(+), 14 deletions(-) create mode 100644 app/src/main/inkscape/ic_bufferlist_arrow_down.svg create mode 100644 app/src/main/inkscape/ic_bufferlist_arrow_up.svg create mode 100644 app/src/main/inkscape/optimized/ic_bufferlist_arrow_down.svg create mode 100644 app/src/main/inkscape/optimized/ic_bufferlist_arrow_up.svg create mode 100644 app/src/main/res/drawable/ic_bufferlist_arrow_down.xml create mode 100644 app/src/main/res/drawable/ic_bufferlist_arrow_up.xml diff --git a/app/src/main/inkscape/ic_bufferlist_arrow_down.svg b/app/src/main/inkscape/ic_bufferlist_arrow_down.svg new file mode 100644 index 00000000..6e5c8d15 --- /dev/null +++ b/app/src/main/inkscape/ic_bufferlist_arrow_down.svg @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/app/src/main/inkscape/ic_bufferlist_arrow_up.svg b/app/src/main/inkscape/ic_bufferlist_arrow_up.svg new file mode 100644 index 00000000..0d075846 --- /dev/null +++ b/app/src/main/inkscape/ic_bufferlist_arrow_up.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/app/src/main/inkscape/optimized/ic_bufferlist_arrow_down.svg b/app/src/main/inkscape/optimized/ic_bufferlist_arrow_down.svg new file mode 100644 index 00000000..0823ab00 --- /dev/null +++ b/app/src/main/inkscape/optimized/ic_bufferlist_arrow_down.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/inkscape/optimized/ic_bufferlist_arrow_up.svg b/app/src/main/inkscape/optimized/ic_bufferlist_arrow_up.svg new file mode 100644 index 00000000..0cf3dce8 --- /dev/null +++ b/app/src/main/inkscape/optimized/ic_bufferlist_arrow_up.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/java/com/ubergeek42/WeechatAndroid/adapters/BufferListAdapter.kt b/app/src/main/java/com/ubergeek42/WeechatAndroid/adapters/BufferListAdapter.kt index 15c4df66..057e795c 100644 --- a/app/src/main/java/com/ubergeek42/WeechatAndroid/adapters/BufferListAdapter.kt +++ b/app/src/main/java/com/ubergeek42/WeechatAndroid/adapters/BufferListAdapter.kt @@ -25,7 +25,7 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.ubergeek42.WeechatAndroid.R -import com.ubergeek42.WeechatAndroid.adapters.BufferListAdapter.* +import com.ubergeek42.WeechatAndroid.adapters.BufferListAdapter.VisualBuffer import com.ubergeek42.WeechatAndroid.databinding.BufferlistItemBinding import com.ubergeek42.WeechatAndroid.relay.Buffer import com.ubergeek42.WeechatAndroid.relay.BufferList @@ -35,7 +35,7 @@ import com.ubergeek42.WeechatAndroid.upload.main import com.ubergeek42.WeechatAndroid.utils.Utils import com.ubergeek42.cats.Kitty import com.ubergeek42.cats.Root -import java.util.* +import java.util.Collections class BufferListAdapter( @@ -206,6 +206,14 @@ class BufferListAdapter( } } + //////////////////////////////////////////////////////////////////////////////////////////// Etc + + @MainThread fun findNextHotBufferPositionOrNull(positionsToSearch: IntProgression) = + positionsToSearch.firstOrNull { position -> + val buffer = buffers.getOrElse(position) { return@firstOrNull false } + buffer.highlights > 0 || (buffer.type == BufferSpec.Type.Private && buffer.unreads != 0) + } + companion object { @Root private val kitty: Kitty = Kitty.make() diff --git a/app/src/main/java/com/ubergeek42/WeechatAndroid/fragments/BufferListFragment.kt b/app/src/main/java/com/ubergeek42/WeechatAndroid/fragments/BufferListFragment.kt index aef670c9..4b6cc510 100644 --- a/app/src/main/java/com/ubergeek42/WeechatAndroid/fragments/BufferListFragment.kt +++ b/app/src/main/java/com/ubergeek42/WeechatAndroid/fragments/BufferListFragment.kt @@ -1,32 +1,35 @@ package com.ubergeek42.WeechatAndroid.fragments import android.content.Context -import com.ubergeek42.WeechatAndroid.relay.BufferListEye -import com.ubergeek42.WeechatAndroid.WeechatActivity -import com.ubergeek42.WeechatAndroid.adapters.BufferListAdapter -import com.ubergeek42.cats.Cat import android.os.Bundle import android.view.LayoutInflater -import android.view.ViewGroup -import org.greenrobot.eventbus.EventBus -import com.ubergeek42.WeechatAndroid.service.P -import org.greenrobot.eventbus.Subscribe -import com.ubergeek42.WeechatAndroid.service.Events.StateChangedEvent -import com.ubergeek42.WeechatAndroid.service.RelayService -import com.ubergeek42.WeechatAndroid.relay.BufferList import android.view.View +import android.view.ViewGroup import androidx.annotation.AnyThread import androidx.annotation.MainThread import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SuppressedLinearLayoutManager +import com.ubergeek42.WeechatAndroid.WeechatActivity +import com.ubergeek42.WeechatAndroid.adapters.BufferListAdapter import com.ubergeek42.WeechatAndroid.databinding.BufferlistBinding +import com.ubergeek42.WeechatAndroid.relay.BufferList +import com.ubergeek42.WeechatAndroid.relay.BufferListEye +import com.ubergeek42.WeechatAndroid.service.Events.StateChangedEvent +import com.ubergeek42.WeechatAndroid.service.P +import com.ubergeek42.WeechatAndroid.service.RelayService import com.ubergeek42.WeechatAndroid.upload.main import com.ubergeek42.WeechatAndroid.utils.afterTextChanged import com.ubergeek42.WeechatAndroid.views.BufferListFragmentFullScreenController import com.ubergeek42.WeechatAndroid.views.FULL_SCREEN_DRAWER_ENABLED import com.ubergeek42.WeechatAndroid.views.FullScreenDrawerLinearLayoutManager +import com.ubergeek42.WeechatAndroid.views.jumpThenSmoothScrollCentering +import com.ubergeek42.cats.Cat import com.ubergeek42.cats.Kitty import com.ubergeek42.cats.Root +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe class BufferListFragment : Fragment(), BufferListEye { companion object { @@ -34,6 +37,7 @@ class BufferListFragment : Fragment(), BufferListEye { } private lateinit var weechatActivity: WeechatActivity + private lateinit var layoutManager: LinearLayoutManager private lateinit var adapter: BufferListAdapter lateinit var ui: BufferlistBinding @@ -58,7 +62,7 @@ class BufferListFragment : Fragment(), BufferListEye { savedInstanceState: Bundle?): View { ui = BufferlistBinding.inflate(inflater) - val layoutManager = if (FULL_SCREEN_DRAWER_ENABLED) { + layoutManager = if (FULL_SCREEN_DRAWER_ENABLED) { FullScreenDrawerLinearLayoutManager(requireContext(), ui.bufferList, adapter) } else { SuppressedLinearLayoutManager(requireContext()) @@ -66,6 +70,14 @@ class BufferListFragment : Fragment(), BufferListEye { ui.bufferList.layoutManager = layoutManager ui.bufferList.adapter = adapter + ui.bufferList.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + showHideArrows() + } + }) + + ui.arrowUp.setOnClickListener { scrollUpToNextHotBuffer() } + ui.arrowDown.setOnClickListener { scrollDownToNextHotBuffer() } ui.filterClear.setOnClickListener { ui.filterInput.text = null @@ -127,6 +139,7 @@ class BufferListFragment : Fragment(), BufferListEye { val hotCount = BufferList.totalHotMessageCount main { + showHideArrows() if (this.hotCount != hotCount) { this.hotCount = hotCount if (hotCount > 0) ui.bufferList.smoothScrollToPosition(0) @@ -140,6 +153,41 @@ class BufferListFragment : Fragment(), BufferListEye { ////////////////////////////////////////////////////////////////////////////////////////// other //////////////////////////////////////////////////////////////////////////////////////////////// + private fun findNextHotBufferPositionAboveVisibleBuffersOrNull(): Int? { + val firstVisibleBufferPosition = layoutManager.findFirstVisibleItemPosition() + val positionsToSearch = (firstVisibleBufferPosition - 1) downTo 0 + return adapter.findNextHotBufferPositionOrNull(positionsToSearch) + } + + private fun findNextHotBufferPositionBelowVisibleBuffersOrNull(): Int? { + val lastBufferPosition = adapter.itemCount - 1 + val lastVisibleBufferPosition = layoutManager.findLastVisibleItemPosition() + val positionsToSearch = (lastVisibleBufferPosition + 1)..lastBufferPosition + return adapter.findNextHotBufferPositionOrNull(positionsToSearch) + } + + private fun showHideArrows() { + val animate = weechatActivity.isBufferListVisible() + val showArrowUp = findNextHotBufferPositionAboveVisibleBuffersOrNull() != null + val showArrowDown = findNextHotBufferPositionBelowVisibleBuffersOrNull() != null + if (showArrowUp) ui.arrowUp.show(animate) else ui.arrowUp.hide(animate) + if (showArrowDown) ui.arrowDown.show(animate) else ui.arrowDown.hide(animate) + } + + private fun scrollUpToNextHotBuffer() { + findNextHotBufferPositionAboveVisibleBuffersOrNull()?.let { position -> + ui.bufferList.jumpThenSmoothScrollCentering(position) + } + } + + private fun scrollDownToNextHotBuffer() { + findNextHotBufferPositionBelowVisibleBuffersOrNull()?.let { position -> + ui.bufferList.jumpThenSmoothScrollCentering(position) + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + @AnyThread private fun applyFilter() { val text = ui.filterInput.text.toString() adapter.setFilter(text, true) diff --git a/app/src/main/java/com/ubergeek42/WeechatAndroid/views/FloatingImageButton.kt b/app/src/main/java/com/ubergeek42/WeechatAndroid/views/FloatingImageButton.kt index 619675f5..b980e5e4 100644 --- a/app/src/main/java/com/ubergeek42/WeechatAndroid/views/FloatingImageButton.kt +++ b/app/src/main/java/com/ubergeek42/WeechatAndroid/views/FloatingImageButton.kt @@ -10,6 +10,7 @@ import android.view.ViewOutlineProvider import android.view.animation.Animation import android.view.animation.ScaleAnimation import androidx.appcompat.widget.AppCompatImageButton +import com.ubergeek42.WeechatAndroid.service.P import com.ubergeek42.WeechatAndroid.upload.applicationContext import com.ubergeek42.WeechatAndroid.upload.f @@ -72,6 +73,18 @@ class CircularImageButton @JvmOverloads constructor( } +class RectangularImageButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, +) : FloatingImageButton(context, attrs) { + init { + background = null + outlineProvider = RoundedRectangleOutlineProvider(10 * P._1dp) + clipToOutline = true + } +} + + private val pillOutlineProvider = object : ViewOutlineProvider() { override fun getOutline(view: View, outline: Outline) { outline.setRoundRect(0, 0, view.width, view.height, view.height.f / 2) @@ -79,5 +92,12 @@ private val pillOutlineProvider = object : ViewOutlineProvider() { } +class RoundedRectangleOutlineProvider(private val cornerRadius: Float) : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setRoundRect(0, 0, view.width, view.height, cornerRadius) + } +} + + private val shortAnimTime = applicationContext .resources.getInteger(android.R.integer.config_shortAnimTime).toLong() diff --git a/app/src/main/java/com/ubergeek42/WeechatAndroid/views/FullScreenActivityController.kt b/app/src/main/java/com/ubergeek42/WeechatAndroid/views/FullScreenActivityController.kt index 297ac034..f74f2480 100644 --- a/app/src/main/java/com/ubergeek42/WeechatAndroid/views/FullScreenActivityController.kt +++ b/app/src/main/java/com/ubergeek42/WeechatAndroid/views/FullScreenActivityController.kt @@ -148,6 +148,8 @@ class BufferListFragmentFullScreenController(val fragment: BufferListFragment) : if (!FULL_SCREEN_DRAWER_ENABLED) { fragment.ui.bufferList.updateMargins( bottom = if (P.showBufferFilter) filterBarHeight else 0) + fragment.ui.arrowDown.updateMargins( + bottom = if (P.showBufferFilter) filterBarHeight else 0) } else { insetListeners.add(insetListener) insetListener.onInsetsChanged() @@ -163,8 +165,11 @@ class BufferListFragmentFullScreenController(val fragment: BufferListFragment) : val ui = fragment.ui val layoutManager = ui.bufferList.layoutManager as? FullScreenDrawerLinearLayoutManager + ui.arrowUp.updateMargins(top = windowInsets.top) + if (P.showBufferFilter) { ui.bufferList.updateMargins(bottom = filterBarHeight + windowInsets.bottom) + ui.arrowDown.updateMargins(bottom = filterBarHeight + windowInsets.bottom) ui.filterInput.updateMargins(bottom = windowInsets.bottom) ui.filterInput.updatePadding(left = windowInsets.left) @@ -176,6 +181,7 @@ class BufferListFragmentFullScreenController(val fragment: BufferListFragment) : windowInsets.left) } else { ui.bufferList.updateMargins(bottom = 0) + ui.arrowDown.updateMargins(bottom = windowInsets.bottom) layoutManager?.setInsets(windowInsets.top, windowInsets.bottom, diff --git a/app/src/main/res/drawable/ic_bufferlist_arrow_down.xml b/app/src/main/res/drawable/ic_bufferlist_arrow_down.xml new file mode 100644 index 00000000..9afba19d --- /dev/null +++ b/app/src/main/res/drawable/ic_bufferlist_arrow_down.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_bufferlist_arrow_up.xml b/app/src/main/res/drawable/ic_bufferlist_arrow_up.xml new file mode 100644 index 00000000..3e774557 --- /dev/null +++ b/app/src/main/res/drawable/ic_bufferlist_arrow_up.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/bufferlist.xml b/app/src/main/res/layout/bufferlist.xml index dccc79db..3b921ded 100644 --- a/app/src/main/res/layout/bufferlist.xml +++ b/app/src/main/res/layout/bufferlist.xml @@ -15,6 +15,26 @@ android:fadingEdge="none" android:scrollbars="vertical" /> + + + + Date: Fri, 15 Mar 2024 19:26:45 +0000 Subject: [PATCH 4/6] Add an option to not put hot buffers on top MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This changes the boolean preference “Sort buffer list” to a list preference, with an option to only sort buffers by number. This does NOT migrate the old setting! Also, this changes `sortByHotAndMessageCountComparator` to also sort by number. Not sorting by number could result in buffers appearing out of order if they were moved. Not a big issue. Closes #541 --- .../adapters/BufferListAdapter.kt | 27 ++++++++++++++----- .../ubergeek42/WeechatAndroid/service/P.java | 6 ++--- .../WeechatAndroid/utils/Constants.java | 8 ++++-- app/src/main/res/values-de/strings.xml | 5 ---- app/src/main/res/values-fr/strings.xml | 5 ---- app/src/main/res/values-ru/strings.xml | 5 ---- app/src/main/res/values/strings.xml | 9 ++++--- app/src/main/res/values/values.xml | 6 +++++ app/src/main/res/xml/preferences.xml | 12 +++++---- 9 files changed, 48 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/ubergeek42/WeechatAndroid/adapters/BufferListAdapter.kt b/app/src/main/java/com/ubergeek42/WeechatAndroid/adapters/BufferListAdapter.kt index 057e795c..b66dca66 100644 --- a/app/src/main/java/com/ubergeek42/WeechatAndroid/adapters/BufferListAdapter.kt +++ b/app/src/main/java/com/ubergeek42/WeechatAndroid/adapters/BufferListAdapter.kt @@ -32,6 +32,9 @@ import com.ubergeek42.WeechatAndroid.relay.BufferList import com.ubergeek42.WeechatAndroid.relay.BufferSpec import com.ubergeek42.WeechatAndroid.service.P import com.ubergeek42.WeechatAndroid.upload.main +import com.ubergeek42.WeechatAndroid.utils.Constants.PREF_SORT_BUFFER_LIST_BY_HOT_MESSAGES_THEN_BY_NUMBER +import com.ubergeek42.WeechatAndroid.utils.Constants.PREF_SORT_BUFFER_LIST_BY_HOT_MESSAGES_THEN_BY_OTHER_MESSAGES_THEN_BY_NUMBER +import com.ubergeek42.WeechatAndroid.utils.Constants.PREF_SORT_BUFFER_LIST_BY_NUMBER import com.ubergeek42.WeechatAndroid.utils.Utils import com.ubergeek42.cats.Kitty import com.ubergeek42.cats.Root @@ -135,12 +138,15 @@ class BufferListAdapter( newBuffers.add(VisualBuffer.fromBuffer(buffer)) } - if (P.sortBuffers) { - Collections.sort(newBuffers, sortByHotAndMessageCountComparator) - } else { - Collections.sort(newBuffers, sortByHotCountAndNumberComparator) + val bufferComparator = when (P.sortBufferList) { + PREF_SORT_BUFFER_LIST_BY_NUMBER -> sortByNumberComparator + PREF_SORT_BUFFER_LIST_BY_HOT_MESSAGES_THEN_BY_NUMBER -> sortByHotMessagesThenByNumberComparator + PREF_SORT_BUFFER_LIST_BY_HOT_MESSAGES_THEN_BY_OTHER_MESSAGES_THEN_BY_NUMBER -> sortByHotMessagesThenByOtherMessagesThenByNumberComparator + else -> sortByNumberComparator } + Collections.sort(newBuffers, bufferComparator) + val diffResult = DiffUtil.calculateDiff(DiffCallback(buffers, newBuffers), false) main { @@ -222,7 +228,11 @@ class BufferListAdapter( } -private val sortByHotCountAndNumberComparator = Comparator { left, right -> +private val sortByNumberComparator = Comparator { left, right -> + left.number - right.number +} + +private val sortByHotMessagesThenByNumberComparator = Comparator { left, right -> val highlightDiff = right.highlights - left.highlights if (highlightDiff != 0) return@Comparator highlightDiff @@ -235,7 +245,7 @@ private val sortByHotCountAndNumberComparator = Comparator { left, } -private val sortByHotAndMessageCountComparator = Comparator { left, right -> +private val sortByHotMessagesThenByOtherMessagesThenByNumberComparator = Comparator { left, right -> val highlightDiff = right.highlights - left.highlights if (highlightDiff != 0) return@Comparator highlightDiff @@ -244,5 +254,8 @@ private val sortByHotAndMessageCountComparator = Comparator { left val pmDiff = pmRight - pmLeft if (pmDiff != 0) return@Comparator pmDiff - right.unreads - left.unreads + val unreadsDiff = right.unreads - left.unreads + if (unreadsDiff != 0) return@Comparator unreadsDiff + + left.number - right.number } \ No newline at end of file diff --git a/app/src/main/java/com/ubergeek42/WeechatAndroid/service/P.java b/app/src/main/java/com/ubergeek42/WeechatAndroid/service/P.java index 5384156f..81f66793 100644 --- a/app/src/main/java/com/ubergeek42/WeechatAndroid/service/P.java +++ b/app/src/main/java/com/ubergeek42/WeechatAndroid/service/P.java @@ -105,7 +105,7 @@ public static void storeThemeOrColorSchemeColors(Context context) { final public static float _4dp = 4 * _1dp; final public static float _200dp = 200 * _1dp; - public static boolean sortBuffers; + public static String sortBufferList; public static boolean filterBuffers; public static boolean hideHiddenBuffers; public static boolean optimizeTraffic; @@ -158,7 +158,7 @@ static VolumeRole fromString(String value) { @MainThread private static void loadUIPreferences() { // buffer list preferences - sortBuffers = p.getBoolean(PREF_SORT_BUFFERS, PREF_SORT_BUFFERS_D); + sortBufferList = p.getString(PREF_SORT_BUFFER_LIST, PREF_SORT_BUFFER_LIST_D); filterBuffers = p.getBoolean(PREF_FILTER_NONHUMAN_BUFFERS, PREF_FILTER_NONHUMAN_BUFFERS_D); hideHiddenBuffers = p.getBoolean(PREF_HIDE_HIDDEN_BUFFERS, PREF_HIDE_HIDDEN_BUFFERS_D); optimizeTraffic = p.getBoolean(PREF_OPTIMIZE_TRAFFIC, PREF_OPTIMIZE_TRAFFIC_D); // okay this is out of sync with onChanged stuff—used for the bell icon @@ -329,7 +329,7 @@ public static void loadServerKeyVerifier() { @MainThread @Override @CatD public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { switch (key) { // buffer list preferences - case PREF_SORT_BUFFERS: sortBuffers = p.getBoolean(key, PREF_SORT_BUFFERS_D); break; + case PREF_SORT_BUFFER_LIST: sortBufferList = p.getString(key, PREF_SORT_BUFFER_LIST_D); break; case PREF_FILTER_NONHUMAN_BUFFERS: filterBuffers = p.getBoolean(key, PREF_FILTER_NONHUMAN_BUFFERS_D); break; case PREF_HIDE_HIDDEN_BUFFERS: hideHiddenBuffers = p.getBoolean(key, PREF_HIDE_HIDDEN_BUFFERS_D); break; case PREF_AUTO_HIDE_ACTIONBAR: autoHideActionbar = p.getBoolean(key, PREF_AUTO_HIDE_ACTIONBAR_D); break; diff --git a/app/src/main/java/com/ubergeek42/WeechatAndroid/utils/Constants.java b/app/src/main/java/com/ubergeek42/WeechatAndroid/utils/Constants.java index b8145a25..a8d844a9 100644 --- a/app/src/main/java/com/ubergeek42/WeechatAndroid/utils/Constants.java +++ b/app/src/main/java/com/ubergeek42/WeechatAndroid/utils/Constants.java @@ -83,8 +83,12 @@ public class Constants { // buffer list public static final String PREF_BUFFERLIST_GROUP = "bufferlist_group"; - public static final String PREF_SORT_BUFFERS = "sort_buffers"; - final public static boolean PREF_SORT_BUFFERS_D = false; + public static final String PREF_SORT_BUFFER_LIST = "sort_buffer_list"; + public static final String PREF_SORT_BUFFER_LIST_BY_NUMBER = "by_number"; + public static final String PREF_SORT_BUFFER_LIST_BY_HOT_MESSAGES_THEN_BY_NUMBER = "by_hot_messages_then_by_number"; + public static final String PREF_SORT_BUFFER_LIST_BY_HOT_MESSAGES_THEN_BY_OTHER_MESSAGES_THEN_BY_NUMBER = "by_hot_messages_then_by_other_messages_then_by_number"; + final public static String PREF_SORT_BUFFER_LIST_D = PREF_SORT_BUFFER_LIST_BY_NUMBER; + public static final String PREF_HIDE_HIDDEN_BUFFERS = "hide_hidden_buffers"; final public static boolean PREF_HIDE_HIDDEN_BUFFERS_D = true; public static final String PREF_FILTER_NONHUMAN_BUFFERS = "filter_nonhuman_buffers"; diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index f8615d87..0e1d12c1 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -577,11 +577,6 @@ Buffer-Liste - - Liste der Buffer sortieren - - Sortierung nach Anzahl der Highlights/privaten Nachrichten/ungelesenen Nachrichten - Ausblenden von Buffern in denen man nicht schreibt diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 1ba2625d..4f5f0748 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -569,11 +569,6 @@ Liste des tampons - - Trier la liste des tampons - - Trier par le nombre de messages notables/messages privés/messages non lus - Cacher les tampons hors salons diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 41c52788..6c0baf74 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -575,11 +575,6 @@ Список буферов - - Сортировать список буферов - - Сортировать по количество упоминаний и новых сообщений в приватных буферах - Скрывать буферы без людей diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 92da247b..bc24a0fc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -572,10 +572,13 @@ Buffer list - + Sort buffer list - - Sort by number of highlights/private messages/unread messages + + By number + By hot messages, then by number + By hot messages, then by other messages, then by number + Hide non-conversation buffers diff --git a/app/src/main/res/values/values.xml b/app/src/main/res/values/values.xml index 832b95ec..23016d78 100644 --- a/app/src/main/res/values/values.xml +++ b/app/src/main/res/values/values.xml @@ -3,6 +3,12 @@ + + by_number + by_hot_messages_then_by_number + by_hot_messages_then_by_other_messages_then_by_number + + left right diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index dd0da1c8..14c6748e 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -196,11 +196,13 @@ - + Date: Sun, 17 Mar 2024 11:54:22 +0000 Subject: [PATCH 5/6] In buffer list, only scroll to *new* hot buffers Before, we would scroll to top whenever we had *any* hot buffers after any change in buffer list. Now, we only scroll only when: * Buffer list is not visible * A buffer becomes hot when it wasn't hot before The target buffer is centered if possible. Also, the badge on the bell icon now shows the number of hot buffers, not the number of new hot messages. --- .../WeechatAndroid/WeechatActivity.kt | 7 ++-- .../adapters/BufferListAdapter.kt | 9 ++++ .../fragments/BufferListFragment.kt | 42 ++++++++++++++----- .../WeechatAndroid/views/ViewUtils.kt | 11 +++++ 4 files changed, 56 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/ubergeek42/WeechatAndroid/WeechatActivity.kt b/app/src/main/java/com/ubergeek42/WeechatAndroid/WeechatActivity.kt index cb9d94fe..6f9c07da 100644 --- a/app/src/main/java/com/ubergeek42/WeechatAndroid/WeechatActivity.kt +++ b/app/src/main/java/com/ubergeek42/WeechatAndroid/WeechatActivity.kt @@ -13,7 +13,6 @@ // limitations under the License. package com.ubergeek42.WeechatAndroid -import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.content.res.Configuration @@ -65,8 +64,8 @@ import com.ubergeek42.WeechatAndroid.service.Events.StateChangedEvent import com.ubergeek42.WeechatAndroid.service.P import com.ubergeek42.WeechatAndroid.service.RelayService import com.ubergeek42.WeechatAndroid.service.getSystemTrustedCertificateChain -import com.ubergeek42.WeechatAndroid.service.showAlarmPermissionRationaleDialog import com.ubergeek42.WeechatAndroid.service.shouldRequestExactAlarmPermission +import com.ubergeek42.WeechatAndroid.service.showAlarmPermissionRationaleDialog import com.ubergeek42.WeechatAndroid.upload.Config import com.ubergeek42.WeechatAndroid.upload.InsertAt import com.ubergeek42.WeechatAndroid.upload.ShareObject @@ -107,7 +106,7 @@ import java.security.cert.CertPathValidatorException import java.security.cert.CertificateException import java.security.cert.CertificateExpiredException import java.security.cert.CertificateNotYetValidException -import java.util.* +import java.util.EnumSet import javax.net.ssl.SSLPeerUnverifiedException import kotlin.system.exitProcess @@ -660,6 +659,8 @@ class WeechatActivity : AppCompatActivity(), CutePageChangeListener, } } + fun isBufferListVisible() = !slidy || isPagerNoticeablyObscured + // set the kitty image that appears when no pages are open private var kittyImageResourceId = -1 @MainThread @Cat private fun setKittyImage(resourceId: Int) { diff --git a/app/src/main/java/com/ubergeek42/WeechatAndroid/adapters/BufferListAdapter.kt b/app/src/main/java/com/ubergeek42/WeechatAndroid/adapters/BufferListAdapter.kt index b66dca66..1fb579ab 100644 --- a/app/src/main/java/com/ubergeek42/WeechatAndroid/adapters/BufferListAdapter.kt +++ b/app/src/main/java/com/ubergeek42/WeechatAndroid/adapters/BufferListAdapter.kt @@ -220,6 +220,15 @@ class BufferListAdapter( buffer.highlights > 0 || (buffer.type == BufferSpec.Type.Private && buffer.unreads != 0) } + @MainThread fun findAllHotBufferIds() = + buffers.mapNotNull { buffer -> + val hot = buffer.highlights > 0 || (buffer.type == BufferSpec.Type.Private && buffer.unreads != 0) + if (hot) buffer.pointer else null + } + + // Returns -1 if not found + @MainThread fun findPositionByBufferId(id: Long): Int = buffers.indexOfFirst { it.pointer == id } + companion object { @Root private val kitty: Kitty = Kitty.make() diff --git a/app/src/main/java/com/ubergeek42/WeechatAndroid/fragments/BufferListFragment.kt b/app/src/main/java/com/ubergeek42/WeechatAndroid/fragments/BufferListFragment.kt index 4b6cc510..cb965033 100644 --- a/app/src/main/java/com/ubergeek42/WeechatAndroid/fragments/BufferListFragment.kt +++ b/app/src/main/java/com/ubergeek42/WeechatAndroid/fragments/BufferListFragment.kt @@ -25,6 +25,7 @@ import com.ubergeek42.WeechatAndroid.views.BufferListFragmentFullScreenControlle import com.ubergeek42.WeechatAndroid.views.FULL_SCREEN_DRAWER_ENABLED import com.ubergeek42.WeechatAndroid.views.FullScreenDrawerLinearLayoutManager import com.ubergeek42.WeechatAndroid.views.jumpThenSmoothScrollCentering +import com.ubergeek42.WeechatAndroid.views.scrollCenteringWithoutAnimation import com.ubergeek42.cats.Cat import com.ubergeek42.cats.Kitty import com.ubergeek42.cats.Root @@ -128,24 +129,45 @@ class BufferListFragment : Fragment(), BufferListEye { ////////////////////////////////////////////////////////////////////////////////// BufferListEye //////////////////////////////////////////////////////////////////////////////////////////////// - @Volatile private var hotCount = 0 - - // if hot count has changed and > 0, scroll to top. note that if the scroll operation is not - // posted, it cat try to scroll to the view that was at position 0 before the diff update + private var hotBufferIds: Collection = emptyList() + + // If the drawer is hidden, and if we've got *new* hot buffers, pick one and center it. + // If the drawer is visible, do not scroll anywhere, + // instead relying on the arrows for hot buffer indication. + // + // To calculate the “diff”, we use buffer IDs, not their positions, + // otherwise a hot buffer that's been merely moved would appear as a new hot buffer. + // + // Note: the `main` block below is run on *every* change in buffers, + // but the adapter might skip some final updates (on main thread) + // if its `onBuffersChanged` is called again soon (usually, on another worker thread). + // // todo don't update on every change? // todo move hotlist updates to the activity @AnyThread @Cat override fun onBuffersChanged() { adapter.onBuffersChanged() + val hotBufferCount = BufferList.hotBufferCount - val hotCount = BufferList.totalHotMessageCount main { - showHideArrows() - if (this.hotCount != hotCount) { - this.hotCount = hotCount - if (hotCount > 0) ui.bufferList.smoothScrollToPosition(0) + val newHotBufferIds = adapter.findAllHotBufferIds() + + if (weechatActivity.isBufferListVisible()) { + showHideArrows() + } else { + val oneNewHotBufferId = newHotBufferIds.firstOrNull { it !in hotBufferIds } + if (oneNewHotBufferId != null) { + val position = adapter.findPositionByBufferId(oneNewHotBufferId) + if (position != -1) { + ui.bufferList.scrollCenteringWithoutAnimation(position) + } + } else { + showHideArrows() + } } - weechatActivity.updateHotCount(hotCount) + + weechatActivity.updateHotCount(hotBufferCount) weechatActivity.onBuffersChanged() + hotBufferIds = newHotBufferIds } } diff --git a/app/src/main/java/com/ubergeek42/WeechatAndroid/views/ViewUtils.kt b/app/src/main/java/com/ubergeek42/WeechatAndroid/views/ViewUtils.kt index d2483a23..2e137037 100644 --- a/app/src/main/java/com/ubergeek42/WeechatAndroid/views/ViewUtils.kt +++ b/app/src/main/java/com/ubergeek42/WeechatAndroid/views/ViewUtils.kt @@ -83,6 +83,17 @@ fun RecyclerView.jumpThenSmoothScrollCentering(position: Int) { } +// A bit hacky, but should be safe. Assumes that height of all children is constant. +fun RecyclerView.scrollCenteringWithoutAnimation(position: Int) { + val layoutManager = layoutManager as? LinearLayoutManager ?: return + val childHeight = getChildAt(0)?.height ?: 0 + val originalItemAnimator = itemAnimator + itemAnimator = null + layoutManager.scrollToPositionWithOffset(position, height / 2 - childHeight / 2 ) + post { itemAnimator = originalItemAnimator } +} + + private class CenteringSmoothScroller(context: Context) : LinearSmoothScroller(context) { override fun calculateDtToFit(viewStart: Int, viewEnd: Int, boxStart: Int, boxEnd: Int, From a5ae9beeff4ca3bedd0f0d4a7d43b9b203706550 Mon Sep 17 00:00:00 2001 From: oakkitten Date: Sun, 17 Mar 2024 13:52:50 +0000 Subject: [PATCH 6/6] Streamline bell icon badge updates The code is spaghetti, sigh. --- .../ubergeek42/WeechatAndroid/WeechatActivity.kt | 2 +- .../fragments/BufferListFragment.kt | 16 +++------------- .../WeechatAndroid/relay/BufferList.kt | 2 -- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/ubergeek42/WeechatAndroid/WeechatActivity.kt b/app/src/main/java/com/ubergeek42/WeechatAndroid/WeechatActivity.kt index 6f9c07da..472212d8 100644 --- a/app/src/main/java/com/ubergeek42/WeechatAndroid/WeechatActivity.kt +++ b/app/src/main/java/com/ubergeek42/WeechatAndroid/WeechatActivity.kt @@ -266,7 +266,6 @@ class WeechatActivity : AppCompatActivity(), CutePageChangeListener, Network.get().register(this, null) // no callback, simply make sure that network info is correct while we are showing EventBus.getDefault().register(this) connectionState = EventBus.getDefault().getStickyEvent(StateChangedEvent::class.java).state - updateHotCount(BufferList.totalHotMessageCount) started = true P.storeThemeOrColorSchemeColors(this) applyColorSchemeToViews() @@ -497,6 +496,7 @@ class WeechatActivity : AppCompatActivity(), CutePageChangeListener, uiMenu = menu updateMenuItems() makeMenuReflectConnectionStatus() + // `onCreateOptionsMenu` is called *after* onStart, when `updateHotCount` was already called updateHotCount(hotNumber) return super.onCreateOptionsMenu(menu) } diff --git a/app/src/main/java/com/ubergeek42/WeechatAndroid/fragments/BufferListFragment.kt b/app/src/main/java/com/ubergeek42/WeechatAndroid/fragments/BufferListFragment.kt index cb965033..8c931c92 100644 --- a/app/src/main/java/com/ubergeek42/WeechatAndroid/fragments/BufferListFragment.kt +++ b/app/src/main/java/com/ubergeek42/WeechatAndroid/fragments/BufferListFragment.kt @@ -16,9 +16,7 @@ import com.ubergeek42.WeechatAndroid.adapters.BufferListAdapter import com.ubergeek42.WeechatAndroid.databinding.BufferlistBinding import com.ubergeek42.WeechatAndroid.relay.BufferList import com.ubergeek42.WeechatAndroid.relay.BufferListEye -import com.ubergeek42.WeechatAndroid.service.Events.StateChangedEvent import com.ubergeek42.WeechatAndroid.service.P -import com.ubergeek42.WeechatAndroid.service.RelayService import com.ubergeek42.WeechatAndroid.upload.main import com.ubergeek42.WeechatAndroid.utils.afterTextChanged import com.ubergeek42.WeechatAndroid.views.BufferListFragmentFullScreenController @@ -29,8 +27,6 @@ import com.ubergeek42.WeechatAndroid.views.scrollCenteringWithoutAnimation import com.ubergeek42.cats.Cat import com.ubergeek42.cats.Kitty import com.ubergeek42.cats.Root -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe class BufferListFragment : Fragment(), BufferListEye { companion object { @@ -84,6 +80,7 @@ class BufferListFragment : Fragment(), BufferListEye { ui.filterInput.text = null } + // The below callback will be called on view restoration ui.filterInput.afterTextChanged { applyFilter() adapter.onBuffersChanged() @@ -94,26 +91,19 @@ class BufferListFragment : Fragment(), BufferListEye { @MainThread @Cat override fun onStart() { super.onStart() - EventBus.getDefault().register(this) ui.filterInput.visibility = if (P.showBufferFilter) View.VISIBLE else View.GONE applyColorSchemeToViews() + attachToBufferList() } @MainThread @Cat override fun onStop() { super.onStop() detachFromBufferList() - EventBus.getDefault().unregister(this) } //////////////////////////////////////////////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////////////////// event - //////////////////////////////////////////////////////////////////////////////////////////////// - - @Subscribe(sticky = true) @AnyThread @Cat fun onEvent(event: StateChangedEvent) { - if (event.state.contains(RelayService.STATE.LISTED)) attachToBufferList() - } - ////////////////////////////////////////////////////////////////////////////////////// the juice + //////////////////////////////////////////////////////////////////////////////////////////////// @AnyThread private fun attachToBufferList() { BufferList.bufferListEye = this diff --git a/app/src/main/java/com/ubergeek42/WeechatAndroid/relay/BufferList.kt b/app/src/main/java/com/ubergeek42/WeechatAndroid/relay/BufferList.kt index 44881f96..c16baad7 100644 --- a/app/src/main/java/com/ubergeek42/WeechatAndroid/relay/BufferList.kt +++ b/app/src/main/java/com/ubergeek42/WeechatAndroid/relay/BufferList.kt @@ -61,8 +61,6 @@ object BufferList { return buffers.firstOrNull { it.pointer == pointer } } - val totalHotMessageCount get() = buffers.sumOf { it.hotCount } - /////////////////////////////////////////////////////////////////////////////////////// handlers private val handlers = ConcurrentHashMap()