Display notification filter/clear actions as menu items (#3877)
Previously the notification filter and clear actions were shown as buttons in the UI, with a preference that determined whether they were displayed. Remove this preference, and display them as menu items. - "Filter notifications" is shown as an icon, if possible - "Clear notifications" is only ever shown as a menu item, to reduce the chance the user inadvertently selects it To ensure that the options menu appears correctly, remove the code that creates a "fake" action bar, and adjust the layouts so that there are three toolbars; - mainToolbar -- displays the icons, and the current "location" (Home, Notifications, etc) - topNav -- displays the row of tabs at the top - bottomNav -- displays the row of tabs at the bottom Only one of them is set as the support action bar (depending on the user's preferences). This provides the "show a logo" and "show the options menu" functionality as standard, without needing to re-implement as the previous code did.
This commit is contained in:
parent
c7ffc6ad93
commit
059352f471
56 changed files with 152 additions and 304 deletions
|
|
@ -16,6 +16,7 @@
|
|||
package com.keylesspalace.tusky
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
|
|
@ -34,6 +35,7 @@ import android.view.KeyEvent
|
|||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.MenuItem.SHOW_AS_ACTION_NEVER
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
|
|
@ -46,6 +48,8 @@ import androidx.core.content.IntentCompat
|
|||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.view.forEach
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.viewpager2.widget.MarginPageTransformer
|
||||
|
|
@ -102,7 +106,6 @@ import com.keylesspalace.tusky.util.show
|
|||
import com.keylesspalace.tusky.util.unsafeLazy
|
||||
import com.keylesspalace.tusky.util.updateShortcut
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
|
|
@ -177,6 +180,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
/** Adapter for the different timeline tabs */
|
||||
private lateinit var tabAdapter: MainPagerAdapter
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
|
@ -253,7 +257,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
}
|
||||
window.statusBarColor = Color.TRANSPARENT // don't draw a status bar, the DrawerLayout and the MaterialDrawerLayout have their own
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.mainToolbar)
|
||||
|
||||
glide = Glide.with(this)
|
||||
|
||||
|
|
@ -262,8 +265,21 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
startActivity(composeIntent)
|
||||
}
|
||||
|
||||
// Determine which of the three toolbars should be the supportActionBar (which hosts
|
||||
// the options menu).
|
||||
val hideTopToolbar = preferences.getBoolean(PrefKeys.HIDE_TOP_TOOLBAR, false)
|
||||
binding.mainToolbar.visible(!hideTopToolbar)
|
||||
if (hideTopToolbar) {
|
||||
when (preferences.getString(PrefKeys.MAIN_NAV_POSITION, "top")) {
|
||||
"top" -> setSupportActionBar(binding.topNav)
|
||||
"bottom" -> setSupportActionBar(binding.bottomNav)
|
||||
}
|
||||
binding.mainToolbar.hide()
|
||||
// There's not enough space in the top/bottom bars to show the title as well.
|
||||
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||
} else {
|
||||
setSupportActionBar(binding.mainToolbar)
|
||||
binding.mainToolbar.show()
|
||||
}
|
||||
|
||||
loadDrawerAvatar(activeAccount.profilePictureUrl, true)
|
||||
|
||||
|
|
@ -361,6 +377,14 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
}
|
||||
}
|
||||
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
super.onPrepareMenu(menu)
|
||||
|
||||
// If the main toolbar is hidden then there's no space in the top/bottomNav to show
|
||||
// the menu items as icons, so forceably disable them
|
||||
if (!binding.mainToolbar.isVisible) menu.forEach { it.setShowAsAction(SHOW_AS_ACTION_NEVER) }
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_search -> {
|
||||
|
|
@ -458,8 +482,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
val drawerOpenClickListener = View.OnClickListener { binding.mainDrawerLayout.open() }
|
||||
|
||||
binding.mainToolbar.setNavigationOnClickListener(drawerOpenClickListener)
|
||||
binding.topNavAvatar.setOnClickListener(drawerOpenClickListener)
|
||||
binding.bottomNavAvatar.setOnClickListener(drawerOpenClickListener)
|
||||
binding.topNav.setNavigationOnClickListener(drawerOpenClickListener)
|
||||
binding.bottomNav.setNavigationOnClickListener(drawerOpenClickListener)
|
||||
|
||||
header = AccountHeaderView(this).apply {
|
||||
headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP
|
||||
|
|
@ -894,112 +918,75 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
val hideTopToolbar = preferences.getBoolean(PrefKeys.HIDE_TOP_TOOLBAR, false)
|
||||
val animateAvatars = preferences.getBoolean("animateGifAvatars", false)
|
||||
|
||||
if (hideTopToolbar) {
|
||||
val activeToolbar = if (hideTopToolbar) {
|
||||
val navOnBottom = preferences.getString("mainNavPosition", "top") == "bottom"
|
||||
|
||||
val avatarView = if (navOnBottom) {
|
||||
binding.bottomNavAvatar.show()
|
||||
binding.bottomNavAvatar
|
||||
if (navOnBottom) {
|
||||
binding.bottomNav
|
||||
} else {
|
||||
binding.topNavAvatar.show()
|
||||
binding.topNavAvatar
|
||||
}
|
||||
|
||||
if (animateAvatars) {
|
||||
Glide.with(this)
|
||||
.load(avatarUrl)
|
||||
.placeholder(R.drawable.avatar_default)
|
||||
.into(avatarView)
|
||||
} else {
|
||||
Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(avatarUrl)
|
||||
.placeholder(R.drawable.avatar_default)
|
||||
.into(avatarView)
|
||||
binding.topNav
|
||||
}
|
||||
} else {
|
||||
binding.bottomNavAvatar.hide()
|
||||
binding.topNavAvatar.hide()
|
||||
binding.mainToolbar
|
||||
}
|
||||
|
||||
val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
|
||||
val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
|
||||
|
||||
if (animateAvatars) {
|
||||
glide.asDrawable()
|
||||
.load(avatarUrl)
|
||||
.transform(
|
||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
|
||||
)
|
||||
.apply {
|
||||
if (showPlaceholder) {
|
||||
placeholder(R.drawable.avatar_default)
|
||||
if (animateAvatars) {
|
||||
glide.asDrawable().load(avatarUrl).transform(RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)))
|
||||
.apply {
|
||||
if (showPlaceholder) placeholder(R.drawable.avatar_default)
|
||||
}
|
||||
.into(object : CustomTarget<Drawable>(navIconSize, navIconSize) {
|
||||
|
||||
override fun onLoadStarted(placeholder: Drawable?) {
|
||||
placeholder?.let {
|
||||
activeToolbar.navigationIcon = FixedSizeDrawable(it, navIconSize, navIconSize)
|
||||
}
|
||||
}
|
||||
.into(object : CustomTarget<Drawable>(navIconSize, navIconSize) {
|
||||
|
||||
override fun onLoadStarted(placeholder: Drawable?) {
|
||||
if (placeholder != null) {
|
||||
binding.mainToolbar.navigationIcon =
|
||||
FixedSizeDrawable(placeholder, navIconSize, navIconSize)
|
||||
}
|
||||
}
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
transition: Transition<in Drawable>?
|
||||
) {
|
||||
if (resource is Animatable) resource.start()
|
||||
activeToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize)
|
||||
}
|
||||
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
transition: Transition<in Drawable>?
|
||||
) {
|
||||
if (resource is Animatable) {
|
||||
resource.start()
|
||||
}
|
||||
binding.mainToolbar.navigationIcon =
|
||||
FixedSizeDrawable(resource, navIconSize, navIconSize)
|
||||
}
|
||||
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
if (placeholder != null) {
|
||||
binding.mainToolbar.navigationIcon =
|
||||
FixedSizeDrawable(placeholder, navIconSize, navIconSize)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
glide.asBitmap()
|
||||
.load(avatarUrl)
|
||||
.transform(
|
||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
|
||||
)
|
||||
.apply {
|
||||
if (showPlaceholder) {
|
||||
placeholder(R.drawable.avatar_default)
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
placeholder?.let {
|
||||
activeToolbar.navigationIcon = FixedSizeDrawable(it, navIconSize, navIconSize)
|
||||
}
|
||||
}
|
||||
.into(object : CustomTarget<Bitmap>(navIconSize, navIconSize) {
|
||||
|
||||
override fun onLoadStarted(placeholder: Drawable?) {
|
||||
if (placeholder != null) {
|
||||
binding.mainToolbar.navigationIcon =
|
||||
FixedSizeDrawable(placeholder, navIconSize, navIconSize)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
glide.asBitmap().load(avatarUrl).transform(RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)))
|
||||
.apply {
|
||||
if (showPlaceholder) placeholder(R.drawable.avatar_default)
|
||||
}
|
||||
.into(object : CustomTarget<Bitmap>(navIconSize, navIconSize) {
|
||||
override fun onLoadStarted(placeholder: Drawable?) {
|
||||
placeholder?.let {
|
||||
activeToolbar.navigationIcon = FixedSizeDrawable(it, navIconSize, navIconSize)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResourceReady(
|
||||
resource: Bitmap,
|
||||
transition: Transition<in Bitmap>?
|
||||
) {
|
||||
binding.mainToolbar.navigationIcon = FixedSizeDrawable(
|
||||
BitmapDrawable(resources, resource),
|
||||
navIconSize,
|
||||
navIconSize
|
||||
)
|
||||
}
|
||||
override fun onResourceReady(
|
||||
resource: Bitmap,
|
||||
transition: Transition<in Bitmap>?
|
||||
) {
|
||||
activeToolbar.navigationIcon = FixedSizeDrawable(
|
||||
BitmapDrawable(resources, resource),
|
||||
navIconSize,
|
||||
navIconSize
|
||||
)
|
||||
}
|
||||
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
if (placeholder != null) {
|
||||
binding.mainToolbar.navigationIcon =
|
||||
FixedSizeDrawable(placeholder, navIconSize, navIconSize)
|
||||
}
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
placeholder?.let {
|
||||
activeToolbar.navigationIcon = FixedSizeDrawable(it, navIconSize, navIconSize)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -130,6 +130,12 @@ class TuskyApplication : Application(), HasAndroidInjector {
|
|||
editor.remove(PrefKeys.MEDIA_PREVIEW_ENABLED)
|
||||
}
|
||||
|
||||
if (oldVersion < 2023072401) {
|
||||
// The notifications filter / clear options are shown on a menu, not a separate bar,
|
||||
// the preference to display them is not needed.
|
||||
editor.remove(PrefKeys.Deprecated.SHOW_NOTIFICATIONS_FILTER)
|
||||
}
|
||||
|
||||
editor.putInt(PrefKeys.SCHEMA_VERSION, newVersion)
|
||||
editor.apply()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import android.view.MenuItem
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.DialogFragment
|
||||
|
|
@ -46,7 +45,6 @@ import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
|||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
||||
import at.connyduck.sparkbutton.helpers.Utils
|
||||
import com.google.android.material.appbar.AppBarLayout.ScrollingViewBehavior
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.R
|
||||
|
|
@ -123,21 +121,6 @@ class NotificationsFragment :
|
|||
return inflater.inflate(R.layout.fragment_timeline_notifications, container, false)
|
||||
}
|
||||
|
||||
private fun updateFilterVisibility(showFilter: Boolean) {
|
||||
val params = binding.swipeRefreshLayout.layoutParams as CoordinatorLayout.LayoutParams
|
||||
if (showFilter) {
|
||||
binding.appBarOptions.setExpanded(true, false)
|
||||
binding.appBarOptions.visibility = View.VISIBLE
|
||||
// Set content behaviour to hide filter on scroll
|
||||
params.behavior = ScrollingViewBehavior()
|
||||
} else {
|
||||
binding.appBarOptions.setExpanded(false, false)
|
||||
binding.appBarOptions.visibility = View.GONE
|
||||
// Clear behaviour to hide app bar
|
||||
params.behavior = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun confirmClearNotifications() {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setMessage(R.string.notification_clear_text)
|
||||
|
|
@ -215,8 +198,6 @@ class NotificationsFragment :
|
|||
footer = NotificationsLoadStateAdapter { adapter.retry() }
|
||||
)
|
||||
|
||||
binding.buttonClear.setOnClickListener { confirmClearNotifications() }
|
||||
binding.buttonFilter.setOnClickListener { showFilterDialog() }
|
||||
(binding.recyclerView.itemAnimator as SimpleItemAnimator?)!!.supportsChangeAnimations =
|
||||
false
|
||||
|
||||
|
|
@ -369,10 +350,10 @@ class NotificationsFragment :
|
|||
}
|
||||
}
|
||||
|
||||
// Update filter option visibility from uiState
|
||||
launch {
|
||||
viewModel.uiState.collectLatest { updateFilterVisibility(it.showFilterOptions) }
|
||||
}
|
||||
// Collect the uiState. Nothing is done with it, but if you don't collect it then
|
||||
// accessing viewModel.uiState.value (e.g., when the filter dialog is created)
|
||||
// returns an empty object.
|
||||
launch { viewModel.uiState.collect() }
|
||||
|
||||
// Update status display from statusDisplayOptions. If the new options request
|
||||
// relative time display collect the flow to periodically update the timestamp in the list gui elements.
|
||||
|
|
@ -439,10 +420,17 @@ class NotificationsFragment :
|
|||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.fragment_notifications, menu)
|
||||
val iconColor = MaterialColors.getColor(binding.root, android.R.attr.textColorPrimary)
|
||||
menu.findItem(R.id.action_refresh)?.apply {
|
||||
icon = IconicsDrawable(requireContext(), GoogleMaterial.Icon.gmd_refresh).apply {
|
||||
sizeDp = 20
|
||||
colorInt = MaterialColors.getColor(binding.root, android.R.attr.textColorPrimary)
|
||||
colorInt = iconColor
|
||||
}
|
||||
}
|
||||
menu.findItem(R.id.action_edit_notification_filter)?.apply {
|
||||
icon = IconicsDrawable(requireContext(), GoogleMaterial.Icon.gmd_tune).apply {
|
||||
sizeDp = 20
|
||||
colorInt = iconColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -458,6 +446,14 @@ class NotificationsFragment :
|
|||
viewModel.accept(InfallibleUiAction.LoadNewest)
|
||||
true
|
||||
}
|
||||
R.id.action_edit_notification_filter -> {
|
||||
showFilterDialog()
|
||||
true
|
||||
}
|
||||
R.id.action_clear_notifications -> {
|
||||
confirmClearNotifications()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
|
@ -625,7 +621,6 @@ class NotificationsFragment :
|
|||
|
||||
override fun onReselect() {
|
||||
if (isAdded) {
|
||||
binding.appBarOptions.setExpanded(true, false)
|
||||
layoutManager.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,23 +74,18 @@ data class UiState(
|
|||
/** Filtered notification types */
|
||||
val activeFilter: Set<Notification.Type> = emptySet(),
|
||||
|
||||
/** True if the UI to filter and clear notifications should be shown */
|
||||
val showFilterOptions: Boolean = false,
|
||||
|
||||
/** True if the FAB should be shown while scrolling */
|
||||
val showFabWhileScrolling: Boolean = true
|
||||
)
|
||||
|
||||
/** Preferences the UI reacts to */
|
||||
data class UiPrefs(
|
||||
val showFabWhileScrolling: Boolean,
|
||||
val showFilter: Boolean
|
||||
val showFabWhileScrolling: Boolean
|
||||
) {
|
||||
companion object {
|
||||
/** Relevant preference keys. Changes to any of these trigger a display update */
|
||||
val prefKeys = setOf(
|
||||
PrefKeys.FAB_HIDE,
|
||||
PrefKeys.SHOW_NOTIFICATIONS_FILTER
|
||||
PrefKeys.FAB_HIDE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -495,7 +490,6 @@ class NotificationsViewModel @Inject constructor(
|
|||
uiState = combine(notificationFilter, getUiPrefs()) { filter, prefs ->
|
||||
UiState(
|
||||
activeFilter = filter.filter,
|
||||
showFilterOptions = prefs.showFilter,
|
||||
showFabWhileScrolling = prefs.showFabWhileScrolling
|
||||
)
|
||||
}.stateIn(
|
||||
|
|
@ -544,8 +538,7 @@ class NotificationsViewModel @Inject constructor(
|
|||
.onStart { emit(toPrefs()) }
|
||||
|
||||
private fun toPrefs() = UiPrefs(
|
||||
showFabWhileScrolling = !preferences.getBoolean(PrefKeys.FAB_HIDE, false),
|
||||
showFilter = preferences.getBoolean(PrefKeys.SHOW_NOTIFICATIONS_FILTER, true)
|
||||
showFabWhileScrolling = !preferences.getBoolean(PrefKeys.FAB_HIDE, false)
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -208,13 +208,6 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
isSingleLineTitle = false
|
||||
}
|
||||
|
||||
switchPreference {
|
||||
setDefaultValue(true)
|
||||
key = PrefKeys.SHOW_NOTIFICATIONS_FILTER
|
||||
setTitle(R.string.pref_title_show_notifications_filter)
|
||||
isSingleLineTitle = false
|
||||
}
|
||||
|
||||
switchPreference {
|
||||
setDefaultValue(true)
|
||||
key = PrefKeys.CONFIRM_REBLOGS
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ enum class AppTheme(val value: String) {
|
|||
*
|
||||
* - Adding a new preference that does not change the interpretation of an existing preference
|
||||
*/
|
||||
const val SCHEMA_VERSION = 2023022701
|
||||
const val SCHEMA_VERSION = 2023072401
|
||||
|
||||
object PrefKeys {
|
||||
// Note: not all of these keys are actually used as SharedPreferences keys but we must give
|
||||
|
|
@ -61,7 +61,6 @@ object PrefKeys {
|
|||
const val ANIMATE_GIF_AVATARS = "animateGifAvatars"
|
||||
const val USE_BLURHASH = "useBlurhash"
|
||||
const val SHOW_SELF_USERNAME = "showSelfUsername"
|
||||
const val SHOW_NOTIFICATIONS_FILTER = "showNotificationsFilter"
|
||||
const val SHOW_CARDS_IN_TIMELINES = "showCardsInTimelines"
|
||||
const val CONFIRM_REBLOGS = "confirmReblogs"
|
||||
const val CONFIRM_FAVOURITES = "confirmFavourites"
|
||||
|
|
@ -104,4 +103,9 @@ object PrefKeys {
|
|||
|
||||
/** UI text scaling factor, stored as float, 100 = 100% = no scaling */
|
||||
const val UI_TEXT_SCALE_RATIO = "uiTextScaleRatio"
|
||||
|
||||
/** Keys that are no longer used (e.g., the preference has been removed */
|
||||
object Deprecated {
|
||||
const val SHOW_NOTIFICATIONS_FILTER = "showNotificationsFilter"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue