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:
parent
90537da122
commit
97558667c8
|
@ -61,6 +61,7 @@ import app.pachli.core.network.model.Filter
|
||||||
import app.pachli.core.network.model.Notification
|
import app.pachli.core.network.model.Notification
|
||||||
import app.pachli.core.network.model.Poll
|
import app.pachli.core.network.model.Poll
|
||||||
import app.pachli.core.network.model.Status
|
import app.pachli.core.network.model.Status
|
||||||
|
import app.pachli.core.preferences.TabTapBehaviour
|
||||||
import app.pachli.core.ui.ActionButtonScrollListener
|
import app.pachli.core.ui.ActionButtonScrollListener
|
||||||
import app.pachli.core.ui.BackgroundMessage
|
import app.pachli.core.ui.BackgroundMessage
|
||||||
import app.pachli.core.ui.extensions.getErrorString
|
import app.pachli.core.ui.extensions.getErrorString
|
||||||
|
@ -671,7 +672,10 @@ class NotificationsFragment :
|
||||||
|
|
||||||
override fun onReselect() {
|
override fun onReselect() {
|
||||||
if (isAdded) {
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,7 @@ import app.pachli.core.network.model.Notification
|
||||||
import app.pachli.core.network.model.Poll
|
import app.pachli.core.network.model.Poll
|
||||||
import app.pachli.core.preferences.PrefKeys
|
import app.pachli.core.preferences.PrefKeys
|
||||||
import app.pachli.core.preferences.SharedPreferencesRepository
|
import app.pachli.core.preferences.SharedPreferencesRepository
|
||||||
|
import app.pachli.core.preferences.TabTapBehaviour
|
||||||
import app.pachli.network.FilterModel
|
import app.pachli.network.FilterModel
|
||||||
import app.pachli.usecase.TimelineCases
|
import app.pachli.usecase.TimelineCases
|
||||||
import app.pachli.util.deserialize
|
import app.pachli.util.deserialize
|
||||||
|
@ -82,6 +83,9 @@ data class UiState(
|
||||||
|
|
||||||
/** True if the FAB should be shown while scrolling */
|
/** True if the FAB should be shown while scrolling */
|
||||||
val showFabWhileScrolling: Boolean = true,
|
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 */
|
/** Preferences the UI reacts to */
|
||||||
|
@ -92,6 +96,7 @@ data class UiPrefs(
|
||||||
/** Relevant preference keys. Changes to any of these trigger a display update */
|
/** Relevant preference keys. Changes to any of these trigger a display update */
|
||||||
val prefKeys = setOf(
|
val prefKeys = setOf(
|
||||||
PrefKeys.FAB_HIDE,
|
PrefKeys.FAB_HIDE,
|
||||||
|
PrefKeys.TAB_TAP_BEHAVIOUR,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -384,6 +389,7 @@ class NotificationsViewModel @Inject constructor(
|
||||||
account.lastNotificationId = "0"
|
account.lastNotificationId = "0"
|
||||||
accountManager.saveAccount(account)
|
accountManager.saveAccount(account)
|
||||||
reload.getAndUpdate { it + 1 }
|
reload.getAndUpdate { it + 1 }
|
||||||
|
repository.invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -506,10 +512,11 @@ class NotificationsViewModel @Inject constructor(
|
||||||
getNotifications(filters = action.filter, initialKey = getInitialKey())
|
getNotifications(filters = action.filter, initialKey = getInitialKey())
|
||||||
}.cachedIn(viewModelScope)
|
}.cachedIn(viewModelScope)
|
||||||
|
|
||||||
uiState = combine(notificationFilter, getUiPrefs()) { filter, prefs ->
|
uiState = combine(notificationFilter, getUiPrefs()) { filter, _ ->
|
||||||
UiState(
|
UiState(
|
||||||
activeFilter = filter.filter,
|
activeFilter = filter.filter,
|
||||||
showFabWhileScrolling = prefs.showFabWhileScrolling,
|
showFabWhileScrolling = !sharedPreferencesRepository.getBoolean(PrefKeys.FAB_HIDE, false),
|
||||||
|
tabTapBehaviour = sharedPreferencesRepository.tabTapBehaviour,
|
||||||
)
|
)
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
|
@ -557,10 +564,5 @@ class NotificationsViewModel @Inject constructor(
|
||||||
*/
|
*/
|
||||||
private fun getUiPrefs() = sharedPreferencesRepository.changes
|
private fun getUiPrefs() = sharedPreferencesRepository.changes
|
||||||
.filter { UiPrefs.prefKeys.contains(it) }
|
.filter { UiPrefs.prefKeys.contains(it) }
|
||||||
.map { toPrefs() }
|
.onStart { emit(null) }
|
||||||
.onStart { emit(toPrefs()) }
|
|
||||||
|
|
||||||
private fun toPrefs() = UiPrefs(
|
|
||||||
showFabWhileScrolling = !sharedPreferencesRepository.getBoolean(PrefKeys.FAB_HIDE, false),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,7 @@ import app.pachli.core.preferences.AppTheme
|
||||||
import app.pachli.core.preferences.DownloadLocation
|
import app.pachli.core.preferences.DownloadLocation
|
||||||
import app.pachli.core.preferences.PrefKeys
|
import app.pachli.core.preferences.PrefKeys
|
||||||
import app.pachli.core.preferences.SharedPreferencesRepository
|
import app.pachli.core.preferences.SharedPreferencesRepository
|
||||||
|
import app.pachli.core.preferences.TabTapBehaviour
|
||||||
import app.pachli.core.ui.extensions.await
|
import app.pachli.core.ui.extensions.await
|
||||||
import app.pachli.core.ui.makeIcon
|
import app.pachli.core.ui.makeIcon
|
||||||
import app.pachli.databinding.AccountNotificationDetailsListItemBinding
|
import app.pachli.databinding.AccountNotificationDetailsListItemBinding
|
||||||
|
@ -195,15 +196,6 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||||
icon = makeIcon(GoogleMaterial.Icon.gmd_format_size)
|
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 {
|
listPreference {
|
||||||
setDefaultValue("disambiguate")
|
setDefaultValue("disambiguate")
|
||||||
setEntries(R.array.pref_show_self_username_names)
|
setEntries(R.array.pref_show_self_username_names)
|
||||||
|
@ -285,6 +277,24 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||||
isSingleLineTitle = false
|
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 {
|
switchPreference {
|
||||||
setDefaultValue(true)
|
setDefaultValue(true)
|
||||||
key = PrefKeys.ENABLE_SWIPE_FOR_TABS
|
key = PrefKeys.ENABLE_SWIPE_FOR_TABS
|
||||||
|
@ -292,11 +302,10 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||||
isSingleLineTitle = false
|
isSingleLineTitle = false
|
||||||
}
|
}
|
||||||
|
|
||||||
switchPreference {
|
enumListPreference<TabTapBehaviour> {
|
||||||
setDefaultValue(false)
|
setDefaultValue(TabTapBehaviour.JUMP_TO_NEXT_PAGE)
|
||||||
key = PrefKeys.SHOW_STATS_INLINE
|
setTitle(app.pachli.core.preferences.R.string.pref_title_tab_tap)
|
||||||
setTitle(R.string.pref_title_show_stat_inline)
|
key = PrefKeys.TAB_TAP_BEHAVIOUR
|
||||||
isSingleLineTitle = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,7 @@ import app.pachli.core.navigation.AttachmentViewData
|
||||||
import app.pachli.core.navigation.EditFilterActivityIntent
|
import app.pachli.core.navigation.EditFilterActivityIntent
|
||||||
import app.pachli.core.network.model.Poll
|
import app.pachli.core.network.model.Poll
|
||||||
import app.pachli.core.network.model.Status
|
import app.pachli.core.network.model.Status
|
||||||
|
import app.pachli.core.preferences.TabTapBehaviour
|
||||||
import app.pachli.core.ui.ActionButtonScrollListener
|
import app.pachli.core.ui.ActionButtonScrollListener
|
||||||
import app.pachli.core.ui.BackgroundMessage
|
import app.pachli.core.ui.BackgroundMessage
|
||||||
import app.pachli.core.ui.extensions.getErrorString
|
import app.pachli.core.ui.extensions.getErrorString
|
||||||
|
@ -799,9 +800,15 @@ class TimelineFragment :
|
||||||
|
|
||||||
override fun onReselect() {
|
override fun onReselect() {
|
||||||
if (isAdded) {
|
if (isAdded) {
|
||||||
binding.recyclerView.scrollToPosition(0)
|
when (viewModel.uiState.value.tabTapBehaviour) {
|
||||||
binding.recyclerView.stopScroll()
|
TabTapBehaviour.JUMP_TO_NEXT_PAGE -> {
|
||||||
saveVisibleId()
|
binding.recyclerView.scrollToPosition(0)
|
||||||
|
binding.recyclerView.stopScroll()
|
||||||
|
saveVisibleId()
|
||||||
|
}
|
||||||
|
|
||||||
|
TabTapBehaviour.JUMP_TO_NEWEST -> viewModel.accept(InfallibleUiAction.LoadNewest)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,7 @@ import app.pachli.core.network.model.Poll
|
||||||
import app.pachli.core.network.model.Status
|
import app.pachli.core.network.model.Status
|
||||||
import app.pachli.core.preferences.PrefKeys
|
import app.pachli.core.preferences.PrefKeys
|
||||||
import app.pachli.core.preferences.SharedPreferencesRepository
|
import app.pachli.core.preferences.SharedPreferencesRepository
|
||||||
|
import app.pachli.core.preferences.TabTapBehaviour
|
||||||
import app.pachli.network.FilterModel
|
import app.pachli.network.FilterModel
|
||||||
import app.pachli.usecase.TimelineCases
|
import app.pachli.usecase.TimelineCases
|
||||||
import app.pachli.viewdata.StatusViewData
|
import app.pachli.viewdata.StatusViewData
|
||||||
|
@ -85,6 +86,9 @@ data class UiState(
|
||||||
|
|
||||||
/** True if the timeline should be shown in reverse order (oldest first) */
|
/** True if the timeline should be shown in reverse order (oldest first) */
|
||||||
val reverseTimeline: Boolean,
|
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
|
// 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
|
uiState = sharedPreferencesRepository.changes
|
||||||
.filter { watchedPrefs.contains(it) }
|
.filter { watchedPrefs.contains(it) }
|
||||||
.map {
|
.map {
|
||||||
UiState(
|
UiState(
|
||||||
showFabWhileScrolling = !sharedPreferencesRepository.getBoolean(PrefKeys.FAB_HIDE, false),
|
showFabWhileScrolling = !sharedPreferencesRepository.getBoolean(PrefKeys.FAB_HIDE, false),
|
||||||
reverseTimeline = sharedPreferencesRepository.getBoolean(PrefKeys.LAB_REVERSE_TIMELINE, false),
|
reverseTimeline = sharedPreferencesRepository.getBoolean(PrefKeys.LAB_REVERSE_TIMELINE, false),
|
||||||
|
tabTapBehaviour = sharedPreferencesRepository.tabTapBehaviour,
|
||||||
)
|
)
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
|
@ -420,6 +429,7 @@ abstract class TimelineViewModel(
|
||||||
initialValue = UiState(
|
initialValue = UiState(
|
||||||
showFabWhileScrolling = !sharedPreferencesRepository.getBoolean(PrefKeys.FAB_HIDE, false),
|
showFabWhileScrolling = !sharedPreferencesRepository.getBoolean(PrefKeys.FAB_HIDE, false),
|
||||||
reverseTimeline = sharedPreferencesRepository.getBoolean(PrefKeys.LAB_REVERSE_TIMELINE, false),
|
reverseTimeline = sharedPreferencesRepository.getBoolean(PrefKeys.LAB_REVERSE_TIMELINE, false),
|
||||||
|
tabTapBehaviour = sharedPreferencesRepository.tabTapBehaviour,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import androidx.core.content.edit
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import app.pachli.core.network.model.Notification
|
import app.pachli.core.network.model.Notification
|
||||||
import app.pachli.core.preferences.PrefKeys
|
import app.pachli.core.preferences.PrefKeys
|
||||||
|
import app.pachli.core.preferences.TabTapBehaviour
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -36,6 +37,7 @@ class NotificationsViewModelTestUiState : NotificationsViewModelTestBase() {
|
||||||
private val initialUiState = UiState(
|
private val initialUiState = UiState(
|
||||||
activeFilter = setOf(Notification.Type.FOLLOW),
|
activeFilter = setOf(Notification.Type.FOLLOW),
|
||||||
showFabWhileScrolling = true,
|
showFabWhileScrolling = true,
|
||||||
|
tabTapBehaviour = TabTapBehaviour.JUMP_TO_NEXT_PAGE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -110,6 +110,7 @@ object PrefKeys {
|
||||||
const val USE_PREVIOUS_UNIFIED_PUSH_DISTRIBUTOR = "usePreviousUnifiedPushDistributor"
|
const val USE_PREVIOUS_UNIFIED_PUSH_DISTRIBUTOR = "usePreviousUnifiedPushDistributor"
|
||||||
|
|
||||||
const val DOWNLOAD_LOCATION = "downloadLocation"
|
const val DOWNLOAD_LOCATION = "downloadLocation"
|
||||||
|
const val TAB_TAP_BEHAVIOUR = "tabTapBehaviour"
|
||||||
|
|
||||||
/** Keys that are no longer used (e.g., the preference has been removed */
|
/** Keys that are no longer used (e.g., the preference has been removed */
|
||||||
object Deprecated {
|
object Deprecated {
|
||||||
|
|
|
@ -53,6 +53,9 @@ class SharedPreferencesRepository @Inject constructor(
|
||||||
val downloadLocation: DownloadLocation
|
val downloadLocation: DownloadLocation
|
||||||
get() = getEnum(PrefKeys.DOWNLOAD_LOCATION, DownloadLocation.DOWNLOADS)
|
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
|
// 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
|
// 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
|
// change listener does not create a strong reference to it) and then no more
|
||||||
|
|
|
@ -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),
|
||||||
|
}
|
|
@ -21,9 +21,15 @@
|
||||||
<string name="download_location_downloads">Downloads folder</string>
|
<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_account">Per-account folders, in Downloads folder</string>
|
||||||
<string name="download_location_per_sender">Per-sender 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_light">Light</string>
|
||||||
<string name="app_theme_black">Black</string>
|
<string name="app_theme_black">Black</string>
|
||||||
<string name="app_theme_auto">Automatic at sunset</string>
|
<string name="app_theme_auto">Automatic at sunset</string>
|
||||||
<string name="app_theme_dark">Dark</string>
|
<string name="app_theme_dark">Dark</string>
|
||||||
<string name="app_theme_system">Use system design</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>
|
</resources>
|
||||||
|
|
Loading…
Reference in New Issue