feat: Allow the user to chose behaviour when tapping a tab (#955)

Previously, tapping a tab would jump to the top of the loaded content,
which might trigger a load of a fresh page.

Provide a preference to control this; the default is the current
behaviour, the user can also choose to discard the current content and
load the newest content.

Fixes #939
This commit is contained in:
Nik Clayton 2024-09-27 17:38:15 +02:00 committed by GitHub
parent 90537da122
commit 97558667c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 99 additions and 27 deletions

View File

@ -61,6 +61,7 @@ import app.pachli.core.network.model.Filter
import app.pachli.core.network.model.Notification
import app.pachli.core.network.model.Poll
import app.pachli.core.network.model.Status
import app.pachli.core.preferences.TabTapBehaviour
import app.pachli.core.ui.ActionButtonScrollListener
import app.pachli.core.ui.BackgroundMessage
import app.pachli.core.ui.extensions.getErrorString
@ -671,7 +672,10 @@ class NotificationsFragment :
override fun onReselect() {
if (isAdded) {
layoutManager.scrollToPosition(0)
when (viewModel.uiState.value.tabTapBehaviour) {
TabTapBehaviour.JUMP_TO_NEXT_PAGE -> layoutManager.scrollToPosition(0)
TabTapBehaviour.JUMP_TO_NEWEST -> viewModel.accept(InfallibleUiAction.LoadNewest)
}
}
}

View File

@ -41,6 +41,7 @@ import app.pachli.core.network.model.Notification
import app.pachli.core.network.model.Poll
import app.pachli.core.preferences.PrefKeys
import app.pachli.core.preferences.SharedPreferencesRepository
import app.pachli.core.preferences.TabTapBehaviour
import app.pachli.network.FilterModel
import app.pachli.usecase.TimelineCases
import app.pachli.util.deserialize
@ -82,6 +83,9 @@ data class UiState(
/** True if the FAB should be shown while scrolling */
val showFabWhileScrolling: Boolean = true,
/** User's preference for behaviour when tapping a tab. */
val tabTapBehaviour: TabTapBehaviour = TabTapBehaviour.JUMP_TO_NEXT_PAGE,
)
/** Preferences the UI reacts to */
@ -92,6 +96,7 @@ data class UiPrefs(
/** Relevant preference keys. Changes to any of these trigger a display update */
val prefKeys = setOf(
PrefKeys.FAB_HIDE,
PrefKeys.TAB_TAP_BEHAVIOUR,
)
}
}
@ -384,6 +389,7 @@ class NotificationsViewModel @Inject constructor(
account.lastNotificationId = "0"
accountManager.saveAccount(account)
reload.getAndUpdate { it + 1 }
repository.invalidate()
}
}
@ -506,10 +512,11 @@ class NotificationsViewModel @Inject constructor(
getNotifications(filters = action.filter, initialKey = getInitialKey())
}.cachedIn(viewModelScope)
uiState = combine(notificationFilter, getUiPrefs()) { filter, prefs ->
uiState = combine(notificationFilter, getUiPrefs()) { filter, _ ->
UiState(
activeFilter = filter.filter,
showFabWhileScrolling = prefs.showFabWhileScrolling,
showFabWhileScrolling = !sharedPreferencesRepository.getBoolean(PrefKeys.FAB_HIDE, false),
tabTapBehaviour = sharedPreferencesRepository.tabTapBehaviour,
)
}.stateIn(
scope = viewModelScope,
@ -557,10 +564,5 @@ class NotificationsViewModel @Inject constructor(
*/
private fun getUiPrefs() = sharedPreferencesRepository.changes
.filter { UiPrefs.prefKeys.contains(it) }
.map { toPrefs() }
.onStart { emit(toPrefs()) }
private fun toPrefs() = UiPrefs(
showFabWhileScrolling = !sharedPreferencesRepository.getBoolean(PrefKeys.FAB_HIDE, false),
)
.onStart { emit(null) }
}

View File

@ -54,6 +54,7 @@ import app.pachli.core.preferences.AppTheme
import app.pachli.core.preferences.DownloadLocation
import app.pachli.core.preferences.PrefKeys
import app.pachli.core.preferences.SharedPreferencesRepository
import app.pachli.core.preferences.TabTapBehaviour
import app.pachli.core.ui.extensions.await
import app.pachli.core.ui.makeIcon
import app.pachli.databinding.AccountNotificationDetailsListItemBinding
@ -195,15 +196,6 @@ class PreferencesFragment : PreferenceFragmentCompat() {
icon = makeIcon(GoogleMaterial.Icon.gmd_format_size)
}
listPreference {
setDefaultValue("top")
setEntries(R.array.pref_main_nav_position_options)
setEntryValues(R.array.pref_main_nav_position_values)
key = PrefKeys.MAIN_NAV_POSITION
setSummaryProvider { entry }
setTitle(R.string.pref_main_nav_position)
}
listPreference {
setDefaultValue("disambiguate")
setEntries(R.array.pref_show_self_username_names)
@ -285,6 +277,24 @@ class PreferencesFragment : PreferenceFragmentCompat() {
isSingleLineTitle = false
}
switchPreference {
setDefaultValue(false)
key = PrefKeys.SHOW_STATS_INLINE
setTitle(R.string.pref_title_show_stat_inline)
isSingleLineTitle = false
}
}
preferenceCategory(app.pachli.core.preferences.R.string.pref_category_tabs) {
listPreference {
setDefaultValue("top")
setEntries(R.array.pref_main_nav_position_options)
setEntryValues(R.array.pref_main_nav_position_values)
key = PrefKeys.MAIN_NAV_POSITION
setSummaryProvider { entry }
setTitle(R.string.pref_main_nav_position)
}
switchPreference {
setDefaultValue(true)
key = PrefKeys.ENABLE_SWIPE_FOR_TABS
@ -292,11 +302,10 @@ class PreferencesFragment : PreferenceFragmentCompat() {
isSingleLineTitle = false
}
switchPreference {
setDefaultValue(false)
key = PrefKeys.SHOW_STATS_INLINE
setTitle(R.string.pref_title_show_stat_inline)
isSingleLineTitle = false
enumListPreference<TabTapBehaviour> {
setDefaultValue(TabTapBehaviour.JUMP_TO_NEXT_PAGE)
setTitle(app.pachli.core.preferences.R.string.pref_title_tab_tap)
key = PrefKeys.TAB_TAP_BEHAVIOUR
}
}

View File

@ -67,6 +67,7 @@ import app.pachli.core.navigation.AttachmentViewData
import app.pachli.core.navigation.EditFilterActivityIntent
import app.pachli.core.network.model.Poll
import app.pachli.core.network.model.Status
import app.pachli.core.preferences.TabTapBehaviour
import app.pachli.core.ui.ActionButtonScrollListener
import app.pachli.core.ui.BackgroundMessage
import app.pachli.core.ui.extensions.getErrorString
@ -799,10 +800,16 @@ class TimelineFragment :
override fun onReselect() {
if (isAdded) {
when (viewModel.uiState.value.tabTapBehaviour) {
TabTapBehaviour.JUMP_TO_NEXT_PAGE -> {
binding.recyclerView.scrollToPosition(0)
binding.recyclerView.stopScroll()
saveVisibleId()
}
TabTapBehaviour.JUMP_TO_NEWEST -> viewModel.accept(InfallibleUiAction.LoadNewest)
}
}
}
companion object {

View File

@ -53,6 +53,7 @@ import app.pachli.core.network.model.Poll
import app.pachli.core.network.model.Status
import app.pachli.core.preferences.PrefKeys
import app.pachli.core.preferences.SharedPreferencesRepository
import app.pachli.core.preferences.TabTapBehaviour
import app.pachli.network.FilterModel
import app.pachli.usecase.TimelineCases
import app.pachli.viewdata.StatusViewData
@ -85,6 +86,9 @@ data class UiState(
/** True if the timeline should be shown in reverse order (oldest first) */
val reverseTimeline: Boolean,
/** User's preference for behaviour when tapping a tab. */
val tabTapBehaviour: TabTapBehaviour = TabTapBehaviour.JUMP_TO_NEXT_PAGE,
)
// TODO: Ui* classes are copied from NotificationsViewModel. Not yet sure whether these actions
@ -406,13 +410,18 @@ abstract class TimelineViewModel(
}
}
val watchedPrefs = setOf(PrefKeys.FAB_HIDE, PrefKeys.LAB_REVERSE_TIMELINE)
val watchedPrefs = setOf(
PrefKeys.FAB_HIDE,
PrefKeys.LAB_REVERSE_TIMELINE,
PrefKeys.TAB_TAP_BEHAVIOUR,
)
uiState = sharedPreferencesRepository.changes
.filter { watchedPrefs.contains(it) }
.map {
UiState(
showFabWhileScrolling = !sharedPreferencesRepository.getBoolean(PrefKeys.FAB_HIDE, false),
reverseTimeline = sharedPreferencesRepository.getBoolean(PrefKeys.LAB_REVERSE_TIMELINE, false),
tabTapBehaviour = sharedPreferencesRepository.tabTapBehaviour,
)
}.stateIn(
scope = viewModelScope,
@ -420,6 +429,7 @@ abstract class TimelineViewModel(
initialValue = UiState(
showFabWhileScrolling = !sharedPreferencesRepository.getBoolean(PrefKeys.FAB_HIDE, false),
reverseTimeline = sharedPreferencesRepository.getBoolean(PrefKeys.LAB_REVERSE_TIMELINE, false),
tabTapBehaviour = sharedPreferencesRepository.tabTapBehaviour,
),
)

View File

@ -21,6 +21,7 @@ import androidx.core.content.edit
import app.cash.turbine.test
import app.pachli.core.network.model.Notification
import app.pachli.core.preferences.PrefKeys
import app.pachli.core.preferences.TabTapBehaviour
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -36,6 +37,7 @@ class NotificationsViewModelTestUiState : NotificationsViewModelTestBase() {
private val initialUiState = UiState(
activeFilter = setOf(Notification.Type.FOLLOW),
showFabWhileScrolling = true,
tabTapBehaviour = TabTapBehaviour.JUMP_TO_NEXT_PAGE,
)
@Test

View File

@ -110,6 +110,7 @@ object PrefKeys {
const val USE_PREVIOUS_UNIFIED_PUSH_DISTRIBUTOR = "usePreviousUnifiedPushDistributor"
const val DOWNLOAD_LOCATION = "downloadLocation"
const val TAB_TAP_BEHAVIOUR = "tabTapBehaviour"
/** Keys that are no longer used (e.g., the preference has been removed */
object Deprecated {

View File

@ -53,6 +53,9 @@ class SharedPreferencesRepository @Inject constructor(
val downloadLocation: DownloadLocation
get() = getEnum(PrefKeys.DOWNLOAD_LOCATION, DownloadLocation.DOWNLOADS)
val tabTapBehaviour: TabTapBehaviour
get() = getEnum(PrefKeys.TAB_TAP_BEHAVIOUR, TabTapBehaviour.JUMP_TO_NEXT_PAGE)
// Ensure the listener is retained during minification. If you do not do this the
// field is removed and eventually garbage collected (because registering it as a
// change listener does not create a strong reference to it) and then no more

View File

@ -0,0 +1,28 @@
/*
* Copyright 2024 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli 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 Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/
package app.pachli.core.preferences
/** Behaviour when the user taps on a tab. */
enum class TabTapBehaviour(override val displayResource: Int, override val value: String? = null) :
PreferenceEnum {
/** Jump the user's position to the top, fetching the next page of content. */
JUMP_TO_NEXT_PAGE(R.string.tab_tap_behaviour_jump_to_next_page),
/** Fetch the newest page of content and jump to that. */
JUMP_TO_NEWEST(R.string.tab_tap_behaviour_jump_to_newest),
}

View File

@ -21,9 +21,15 @@
<string name="download_location_downloads">Downloads folder</string>
<string name="download_location_per_account">Per-account folders, in Downloads folder</string>
<string name="download_location_per_sender">Per-sender folders, in Downloads folder</string>
<string name="app_theme_light">Light</string>
<string name="app_theme_black">Black</string>
<string name="app_theme_auto">Automatic at sunset</string>
<string name="app_theme_dark">Dark</string>
<string name="app_theme_system">Use system design</string>
<string name="pref_category_tabs">Tabs</string>
<string name="pref_title_tab_tap">Action when tapping a tab</string>
<string name="tab_tap_behaviour_jump_to_next_page">Jump to next page</string>
<string name="tab_tap_behaviour_jump_to_newest">Jump to newest content</string>
</resources>