Add "Refresh" accessibility menu (#3121)

* Add "Refresh" accessibility menu to TimelineFragment

Per https://developer.android.com/reference/androidx/swiperefreshlayout/widget/SwipeRefreshLayout
the layout does not provide accessibility events, and a menu item should be
provided as an alternative method for refreshing the content.

In `TimelineFragment`:
- Implement the `MenuProvider` interface so it can populate the action bar
  menu in activities that host the fragment
- Create a "Refresh" menu item, and refresh the state when it is selected

`MainActivity` has to change how the menu is created, so that fragments
can add items to it.

In `MainActivity`:
- Call `setSupportActionBar` so `mainToolbar` participates in menus
- Implement the `MenuProvider` interface, and move menu creation there
- Set the title via supportActionBar

* Never show the refresh item as a menubar action

Per guidelines in https://developer.android.com/develop/ui/views/touch-and-input/swipe/add-swipe-interface#AddRefreshAction

* Add "Refresh" menu item for AccountMediaFragment

Also, fix the colour of the refresh progress indicator

* Implement "Refresh" for AnnouncementsActivity

* Add "Refresh" menu for ConversationsFragment

* Keep the tabs adapter over the life of the viewpager

Make `tabs` `var` instead of `val` in `MainPagerAdapter` so it can be updated
when tabs change.

Then detach the `tabLayoutMediator`, update the tabs, and call
`notifyItemRangeChanged` in `setupTabs()`.

This fixes a bug (not sure if it's this code, or in ViewPager2) where
assigning a new adapter to the view pager seemed to result in a leak of one
or more fragments. This wasn't user-visible, but it's a leak, and it becomes
user-visible when fragments want to display menus.

This also fixes two other bugs:

1. Be on the left-most tab. Scroll down a bit. Then modify the tabs at
   "Account preferences > tabs", but keep the left-most tab as-is.

   Then go back to MainActivity. Your reading position in the left-most
   tab has been jumped to the top.

2. Be on any non-left-most tab. Then modify the tab list by reordering tabs
   (adding/removing tabs is also OK).

   Then go back to MainActivity. Your tab selection has been overridden,
   and the left-most tab has been selected.

Because the fragments are not destroyed unnecessarily your reading position
is retained. And it remembers the tab you had selected, and as long as that
tab is still present you will be returned to it, even if it's changed
position in the list.

Fixes https://github.com/tuskyapp/Tusky/issues/3251

* Add "Refresh" menu for ScheduledStatusActivity

* Lint

* Add "Refresh" menu for SearchFragment / SearchActivity

* Explicitly set the searchview width

Using "collapseActionView" requires the user to press "Back" twice to exit
the activity, which is not acceptable.

* Move toolbar handling in to ViewThreadActivity

Previous code had the toolbar in the fragment's layout. Refactor to make
consistent with other activities, and move the toolbar in to the activity
layout.

Implement MenuProvider in ViewThreadFragment to adjust the menu in the
activity.

* Add "Refresh" menu to ViewThreadFragment

* Implement "Refresh" for ViewEditsFragment

* Lint

* Add "Refresh" menu to ReportStatusesFragment

* Add "Refresh" menu to NotificationsFragment

* Rename menu resource files

Be consistent with the layout resource files, which have an activity/fragment
prefix, then the lower_snake_case name of the activity or fragment it's for.

* Only enable refresh menu if swiptorefresh is enabled

Some timelines don't have swipetorefresh enabled (e.g., those shown on
AccountActivity). In those cases don't add the refresh menu, rely on the
hosting activity to provide it.

Update AccountActivity to provide the refresh menu item.
This commit is contained in:
Nik Clayton 2023-03-01 19:58:18 +01:00 committed by GitHub
parent f9588b48e2
commit 1b6108ca94
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 671 additions and 144 deletions

View file

@ -29,6 +29,8 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
@ -40,6 +42,7 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.view.MenuProvider
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@ -135,7 +138,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector { class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector, MenuProvider {
@Inject @Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any> lateinit var androidInjector: DispatchingAndroidInjector<Any>
@ -244,6 +247,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} }
window.statusBarColor = Color.TRANSPARENT // don't draw a status bar, the DrawerLayout and the MaterialDrawerLayout have their own window.statusBarColor = Color.TRANSPARENT // don't draw a status bar, the DrawerLayout and the MaterialDrawerLayout have their own
setContentView(binding.root) setContentView(binding.root)
setSupportActionBar(binding.mainToolbar)
glide = Glide.with(this) glide = Glide.with(this)
@ -257,17 +261,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
loadDrawerAvatar(activeAccount.profilePictureUrl, true) loadDrawerAvatar(activeAccount.profilePictureUrl, true)
binding.mainToolbar.menu.add(R.string.action_search).apply { addMenuProvider(this)
setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
icon = IconicsDrawable(this@MainActivity, GoogleMaterial.Icon.gmd_search).apply {
sizeDp = 20
colorInt = MaterialColors.getColor(binding.mainToolbar, android.R.attr.textColorPrimary)
}
setOnMenuItemClickListener {
startActivity(SearchActivity.getIntent(this@MainActivity))
true
}
}
binding.viewPager.reduceSwipeSensitivity() binding.viewPager.reduceSwipeSensitivity()
@ -352,6 +346,26 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
draftsAlert.observeInContext(this, true) draftsAlert.observeInContext(this, true)
} }
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.activity_main, menu)
menu.findItem(R.id.action_search)?.apply {
icon = IconicsDrawable(this@MainActivity, GoogleMaterial.Icon.gmd_search).apply {
sizeDp = 20
colorInt = MaterialColors.getColor(binding.mainToolbar, android.R.attr.textColorPrimary)
}
}
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_search -> {
startActivity(SearchActivity.getIntent(this@MainActivity))
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
val currentEmojiPack = preferences.getString(EMOJI_PREFERENCE, "") val currentEmojiPack = preferences.getString(EMOJI_PREFERENCE, "")
@ -745,7 +759,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} }
val activeTabPosition = if (selectNotificationTab) notificationTabPosition else 0 val activeTabPosition = if (selectNotificationTab) notificationTabPosition else 0
binding.mainToolbar.title = tabs[activeTabPosition].title(this@MainActivity) supportActionBar?.title = tabs[activeTabPosition].title(this@MainActivity)
binding.mainToolbar.setOnClickListener { binding.mainToolbar.setOnClickListener {
(tabAdapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect() (tabAdapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect()
} }

View file

@ -26,6 +26,7 @@ import android.graphics.drawable.LayerDrawable
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.view.Menu import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -35,6 +36,7 @@ import androidx.annotation.Px
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.MenuProvider
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
@ -88,6 +90,10 @@ import com.keylesspalace.tusky.util.unsafeLazy
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.view.showMuteAccountDialog import com.keylesspalace.tusky.view.showMuteAccountDialog
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import java.text.NumberFormat import java.text.NumberFormat
@ -97,7 +103,7 @@ import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.abs import kotlin.math.abs
class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector, LinkListener { class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider, HasAndroidInjector, LinkListener {
@Inject @Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any> lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
@ -153,6 +159,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
loadResources() loadResources()
makeNotificationBarTransparent() makeNotificationBarTransparent()
setContentView(binding.root) setContentView(binding.root)
addMenuProvider(this)
// Obtain information to fill out the profile. // Obtain information to fill out the profile.
viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!) viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!)
@ -414,14 +421,16 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
draftsAlert.observeInContext(this, true) draftsAlert.observeInContext(this, true)
} }
private fun onRefresh() {
viewModel.refresh()
adapter.refreshContent()
}
/** /**
* Setup swipe to refresh layout * Setup swipe to refresh layout
*/ */
private fun setupRefreshLayout() { private fun setupRefreshLayout() {
binding.swipeToRefreshLayout.setOnRefreshListener { binding.swipeToRefreshLayout.setOnRefreshListener { onRefresh() }
viewModel.refresh()
adapter.refreshContent()
}
viewModel.isRefreshing.observe( viewModel.isRefreshing.observe(
this this
) { isRefreshing -> ) { isRefreshing ->
@ -731,7 +740,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
} }
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.account_toolbar, menu) menuInflater.inflate(R.menu.account_toolbar, menu)
val openAsItem = menu.findItem(R.id.action_open_as) val openAsItem = menu.findItem(R.id.action_open_as)
@ -796,7 +805,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
menu.removeItem(R.id.action_add_or_remove_from_list) menu.removeItem(R.id.action_add_or_remove_from_list)
} }
return super.onCreateOptionsMenu(menu) menu.findItem(R.id.action_search)?.apply {
icon = IconicsDrawable(this@AccountActivity, GoogleMaterial.Icon.gmd_search).apply {
sizeDp = 20
colorInt = MaterialColors.getColor(binding.collapsingToolbar, android.R.attr.textColorPrimary)
}
}
} }
private fun showFollowRequestPendingDialog() { private fun showFollowRequestPendingDialog() {
@ -884,7 +898,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
viewUrl(url) viewUrl(url)
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onMenuItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_open_in_web -> { R.id.action_open_in_web -> {
// If the account isn't loaded yet, eat the input. // If the account isn't loaded yet, eat the input.
@ -949,6 +963,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
viewModel.changeShowReblogsState() viewModel.changeShowReblogsState()
return true return true
} }
R.id.action_refresh -> {
binding.swipeToRefreshLayout.isRefreshing = true
onRefresh()
return true
}
R.id.action_report -> { R.id.action_report -> {
loadedAccount?.let { loadedAccount -> loadedAccount?.let { loadedAccount ->
startActivity(ReportActivity.getIntent(this, viewModel.accountId, loadedAccount.username)) startActivity(ReportActivity.getIntent(this, viewModel.accountId, loadedAccount.username))
@ -956,7 +975,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
return true return true
} }
} }
return super.onOptionsItemSelected(item) return false
} }
override fun getActionButton(): FloatingActionButton? { override fun getActionButton(): FloatingActionButton? {

View file

@ -16,15 +16,21 @@
package com.keylesspalace.tusky.components.account.media package com.keylesspalace.tusky.components.account.media
import android.os.Bundle import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.MenuProvider
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
@ -39,20 +45,22 @@ import com.keylesspalace.tusky.util.openLink
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
/** /**
* Created by charlag on 26/10/2017.
*
* Fragment with multiple columns of media previews for the specified account. * Fragment with multiple columns of media previews for the specified account.
*/ */
class AccountMediaFragment : class AccountMediaFragment :
Fragment(R.layout.fragment_timeline), Fragment(R.layout.fragment_timeline),
RefreshableFragment, RefreshableFragment,
MenuProvider,
Injectable { Injectable {
@Inject @Inject
@ -73,6 +81,7 @@ class AccountMediaFragment :
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
val alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia val alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia
@ -95,6 +104,8 @@ class AccountMediaFragment :
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
binding.swipeRefreshLayout.isEnabled = false binding.swipeRefreshLayout.isEnabled = false
binding.swipeRefreshLayout.setOnRefreshListener { refreshContent() }
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
binding.statusView.visibility = View.GONE binding.statusView.visibility = View.GONE
@ -108,6 +119,10 @@ class AccountMediaFragment :
binding.statusView.hide() binding.statusView.hide()
binding.progressBar.hide() binding.progressBar.hide()
if (loadState.refresh != LoadState.Loading && loadState.source.refresh != LoadState.Loading) {
binding.swipeRefreshLayout.isRefreshing = false
}
if (adapter.itemCount == 0) { if (adapter.itemCount == 0) {
when (loadState.refresh) { when (loadState.refresh) {
is LoadState.NotLoading -> { is LoadState.NotLoading -> {
@ -133,6 +148,27 @@ class AccountMediaFragment :
} }
} }
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.fragment_account_media, menu)
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)
}
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_refresh -> {
binding.swipeRefreshLayout.isRefreshing = true
refreshContent()
true
}
else -> false
}
}
private fun onAttachmentClick(selected: AttachmentViewData, view: View) { private fun onAttachmentClick(selected: AttachmentViewData, view: View) {
if (!selected.isRevealed) { if (!selected.isRevealed) {
viewModel.revealAttachment(selected) viewModel.revealAttachment(selected)

View file

@ -19,12 +19,17 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.PopupWindow import android.widget.PopupWindow
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.view.MenuProvider
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.StatusListActivity import com.keylesspalace.tusky.StatusListActivity
@ -42,9 +47,18 @@ import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.unsafeLazy import com.keylesspalace.tusky.util.unsafeLazy
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.view.EmojiPicker import com.keylesspalace.tusky.view.EmojiPicker
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import javax.inject.Inject import javax.inject.Inject
class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, OnEmojiSelectedListener, Injectable { class AnnouncementsActivity :
BottomSheetActivity(),
AnnouncementActionListener,
OnEmojiSelectedListener,
MenuProvider,
Injectable {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
@ -71,6 +85,7 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(binding.root) setContentView(binding.root)
addMenuProvider(this)
setSupportActionBar(binding.includedToolbar.toolbar) setSupportActionBar(binding.includedToolbar.toolbar)
supportActionBar?.apply { supportActionBar?.apply {
@ -130,6 +145,27 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
binding.progressBar.show() binding.progressBar.show()
} }
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.activity_announcements, menu)
menu.findItem(R.id.action_search)?.apply {
icon = IconicsDrawable(this@AnnouncementsActivity, GoogleMaterial.Icon.gmd_search).apply {
sizeDp = 20
colorInt = MaterialColors.getColor(binding.includedToolbar.toolbar, android.R.attr.textColorPrimary)
}
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_refresh -> {
binding.swipeRefreshLayout.isRefreshing = true
refreshAnnouncements()
true
}
else -> false
}
}
private fun refreshAnnouncements() { private fun refreshAnnouncements() {
viewModel.load() viewModel.load()
binding.swipeRefreshLayout.isRefreshing = true binding.swipeRefreshLayout.isRefreshing = true

View file

@ -17,10 +17,14 @@ package com.keylesspalace.tusky.components.conversation
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.view.MenuProvider
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -32,6 +36,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import at.connyduck.sparkbutton.helpers.Utils import at.connyduck.sparkbutton.helpers.Utils
import autodispose2.androidx.lifecycle.autoDispose import autodispose2.androidx.lifecycle.autoDispose
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.StatusListActivity import com.keylesspalace.tusky.StatusListActivity
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
@ -52,6 +57,10 @@ import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -61,7 +70,12 @@ import javax.inject.Inject
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
import kotlin.time.toDuration import kotlin.time.toDuration
class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment { class ConversationsFragment :
SFragment(),
StatusActionListener,
Injectable,
ReselectableFragment,
MenuProvider {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
@ -82,6 +96,8 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
val preferences = PreferenceManager.getDefaultSharedPreferences(view.context) val preferences = PreferenceManager.getDefaultSharedPreferences(view.context)
val statusDisplayOptions = StatusDisplayOptions( val statusDisplayOptions = StatusDisplayOptions(
@ -189,6 +205,27 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
} }
} }
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.fragment_conversations, menu)
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)
}
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_refresh -> {
binding.swipeRefreshLayout.isRefreshing = true
refreshContent()
true
}
else -> false
}
}
private fun setupRecyclerView() { private fun setupRecyclerView() {
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(context) binding.recyclerView.layoutManager = LinearLayoutManager(context)
@ -200,10 +237,12 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
binding.recyclerView.adapter = adapter.withLoadStateFooter(ConversationLoadStateAdapter(adapter::retry)) binding.recyclerView.adapter = adapter.withLoadStateFooter(ConversationLoadStateAdapter(adapter::retry))
} }
private fun refreshContent() {
adapter.refresh()
}
private fun initSwipeToRefresh() { private fun initSwipeToRefresh() {
binding.swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener { refreshContent() }
adapter.refresh()
}
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
} }

View file

@ -16,17 +16,24 @@
package com.keylesspalace.tusky.components.report.fragments package com.keylesspalace.tusky.components.report.fragments
import android.os.Bundle import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.MenuProvider
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import com.google.android.material.color.MaterialColors
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.StatusListActivity import com.keylesspalace.tusky.StatusListActivity
@ -48,11 +55,20 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Injectable, AdapterHandler { class ReportStatusesFragment :
Fragment(R.layout.fragment_report_statuses),
Injectable,
OnRefreshListener,
MenuProvider,
AdapterHandler {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
@ -90,18 +106,42 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
handleClicks() handleClicks()
initStatusesView() initStatusesView()
setupSwipeRefreshLayout() setupSwipeRefreshLayout()
} }
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.fragment_report_statuses, menu)
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)
}
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_refresh -> {
binding.swipeRefreshLayout.isRefreshing = true
onRefresh()
true
}
else -> false
}
}
override fun onRefresh() {
snackbarErrorRetry?.dismiss()
adapter.refresh()
}
private fun setupSwipeRefreshLayout() { private fun setupSwipeRefreshLayout() {
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
binding.swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener(this)
snackbarErrorRetry?.dismiss()
adapter.refresh()
}
} }
private fun initStatusesView() { private fun initStatusesView() {

View file

@ -18,13 +18,18 @@ package com.keylesspalace.tusky.components.scheduled
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.MenuProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import autodispose2.androidx.lifecycle.autoDispose import autodispose2.androidx.lifecycle.autoDispose
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.EventHub
@ -36,12 +41,21 @@ import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.entity.ScheduledStatus
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class ScheduledStatusActivity : BaseActivity(), ScheduledStatusActionListener, Injectable { class ScheduledStatusActivity :
BaseActivity(),
ScheduledStatusActionListener,
MenuProvider,
Injectable {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
@ -51,13 +65,15 @@ class ScheduledStatusActivity : BaseActivity(), ScheduledStatusActionListener, I
private val viewModel: ScheduledStatusViewModel by viewModels { viewModelFactory } private val viewModel: ScheduledStatusViewModel by viewModels { viewModelFactory }
private val binding by viewBinding(ActivityScheduledStatusBinding::inflate)
private val adapter = ScheduledStatusAdapter(this) private val adapter = ScheduledStatusAdapter(this)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val binding = ActivityScheduledStatusBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
addMenuProvider(this)
setSupportActionBar(binding.includedToolbar.toolbar) setSupportActionBar(binding.includedToolbar.toolbar)
supportActionBar?.run { supportActionBar?.run {
@ -113,6 +129,27 @@ class ScheduledStatusActivity : BaseActivity(), ScheduledStatusActionListener, I
} }
} }
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.activity_announcements, menu)
menu.findItem(R.id.action_search)?.apply {
icon = IconicsDrawable(this@ScheduledStatusActivity, GoogleMaterial.Icon.gmd_search).apply {
sizeDp = 20
colorInt = MaterialColors.getColor(binding.includedToolbar.toolbar, android.R.attr.textColorPrimary)
}
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_refresh -> {
binding.swipeRefreshLayout.isRefreshing = true
refreshStatuses()
true
}
else -> false
}
}
private fun refreshStatuses() { private fun refreshStatuses() {
adapter.refresh() adapter.refresh()
} }

View file

@ -20,8 +20,11 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuProvider
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.BottomSheetActivity
@ -37,7 +40,7 @@ import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import javax.inject.Inject import javax.inject.Inject
class SearchActivity : BottomSheetActivity(), HasAndroidInjector { class SearchActivity : BottomSheetActivity(), HasAndroidInjector, MenuProvider {
@Inject @Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any> lateinit var androidInjector: DispatchingAndroidInjector<Any>
@ -59,6 +62,7 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
setDisplayShowHomeEnabled(true) setDisplayShowHomeEnabled(true)
setDisplayShowTitleEnabled(false) setDisplayShowTitleEnabled(false)
} }
addMenuProvider(this)
setupPages() setupPages()
handleIntent(intent) handleIntent(intent)
} }
@ -81,17 +85,18 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
handleIntent(intent) handleIntent(intent)
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.search_toolbar, menu) menuInflater.inflate(R.menu.search_toolbar, menu)
val searchView = menu.findItem(R.id.action_search) val searchViewMenuItem = menu.findItem(R.id.action_search)
.actionView as SearchView searchViewMenuItem.expandActionView()
val searchView = searchViewMenuItem.actionView as SearchView
setupSearchView(searchView) setupSearchView(searchView)
searchView.setQuery(viewModel.currentQuery, false) searchView.setQuery(viewModel.currentQuery, false)
}
return true override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return false
} }
override fun finish() { override fun finish() {
@ -116,17 +121,42 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
private fun setupSearchView(searchView: SearchView) { private fun setupSearchView(searchView: SearchView) {
searchView.setIconifiedByDefault(false) searchView.setIconifiedByDefault(false)
searchView.setSearchableInfo((getSystemService(Context.SEARCH_SERVICE) as? SearchManager)?.getSearchableInfo(componentName)) searchView.setSearchableInfo((getSystemService(Context.SEARCH_SERVICE) as? SearchManager)?.getSearchableInfo(componentName))
searchView.requestFocus() // SearchView has a bug. If it's displayed 'app:showAsAction="always"' it's too wide,
// pushing other icons (including the options menu '...' icon) off the edge of the
// screen.
//
// E.g., see:
//
// - https://stackoverflow.com/questions/41662373/android-toolbar-searchview-too-wide-to-move-other-items
// - https://stackoverflow.com/questions/51525088/how-to-control-size-of-a-searchview-in-toolbar
// - https://stackoverflow.com/questions/36976163/push-icons-away-when-expandig-searchview-in-android-toolbar
// - https://issuetracker.google.com/issues/36976484
//
// The fix is to use 'app:showAsAction="ifRoom|collapseActionView"' and then immediately
// expand it after inflating. That sets the width correctly.
//
// But if you do that code in AppCompatDelegateImpl activates, and when the user presses
// the "Back" button the SearchView is first set to its collapsed state. The user has to
// press "Back" again to exit the activity. This is clearly unacceptable.
//
// It appears to be impossible to override this behaviour on API level < 33.
//
// SearchView does allow you to specify the maximum width. So take the screen width,
// subtract 48dp * 2 (for the menu icon and back icon on either side), convert to pixels,
// and use that.
val pxScreenWidth = resources.displayMetrics.widthPixels
val pxBuffer = ((48 * 2) * resources.displayMetrics.density).toInt()
searchView.maxWidth = pxScreenWidth - pxBuffer
searchView.maxWidth = Integer.MAX_VALUE searchView.requestFocus()
} }
override fun androidInjector() = androidInjector override fun androidInjector() = androidInjector
companion object { companion object {
const val TAG = "SearchActivity"
fun getIntent(context: Context) = Intent(context, SearchActivity::class.java) fun getIntent(context: Context) = Intent(context, SearchActivity::class.java)
} }
} }

View file

@ -1,9 +1,14 @@
package com.keylesspalace.tusky.components.search.fragments package com.keylesspalace.tusky.components.search.fragments
import android.os.Bundle import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.PagingData import androidx.paging.PagingData
@ -12,6 +17,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.color.MaterialColors
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
@ -25,6 +31,10 @@ import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible 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
import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -34,7 +44,8 @@ abstract class SearchFragment<T : Any> :
Fragment(R.layout.fragment_search), Fragment(R.layout.fragment_search),
LinkListener, LinkListener,
Injectable, Injectable,
SwipeRefreshLayout.OnRefreshListener { SwipeRefreshLayout.OnRefreshListener,
MenuProvider {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
@ -58,6 +69,7 @@ abstract class SearchFragment<T : Any> :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
initAdapter() initAdapter()
setupSwipeRefreshLayout() setupSwipeRefreshLayout()
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
subscribeObservables() subscribeObservables()
} }
@ -95,6 +107,27 @@ abstract class SearchFragment<T : Any> :
} }
} }
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.fragment_timeline, menu)
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)
}
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_refresh -> {
binding.swipeRefreshLayout.isRefreshing = true
onRefresh()
true
}
else -> false
}
}
private fun initAdapter() { private fun initAdapter() {
binding.searchRecyclerView.addItemDecoration(DividerItemDecoration(binding.searchRecyclerView.context, DividerItemDecoration.VERTICAL)) binding.searchRecyclerView.addItemDecoration(DividerItemDecoration(binding.searchRecyclerView.context, DividerItemDecoration.VERTICAL))
binding.searchRecyclerView.layoutManager = LinearLayoutManager(binding.searchRecyclerView.context) binding.searchRecyclerView.layoutManager = LinearLayoutManager(binding.searchRecyclerView.context)

View file

@ -18,10 +18,14 @@ package com.keylesspalace.tusky.components.timeline
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.MenuProvider
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -34,6 +38,7 @@ import androidx.recyclerview.widget.SimpleItemAnimator
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import at.connyduck.sparkbutton.helpers.Utils import at.connyduck.sparkbutton.helpers.Utils
import autodispose2.androidx.lifecycle.autoDispose import autodispose2.androidx.lifecycle.autoDispose
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
@ -65,6 +70,10 @@ import com.keylesspalace.tusky.util.unsafeLazy
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
import com.keylesspalace.tusky.viewdata.StatusViewData import com.keylesspalace.tusky.viewdata.StatusViewData
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Observable
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -79,7 +88,8 @@ class TimelineFragment :
StatusActionListener, StatusActionListener,
Injectable, Injectable,
ReselectableFragment, ReselectableFragment,
RefreshableFragment { RefreshableFragment,
MenuProvider {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
@ -198,6 +208,8 @@ class TimelineFragment :
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
setupSwipeRefreshLayout() setupSwipeRefreshLayout()
setupRecyclerView() setupRecyclerView()
@ -293,6 +305,35 @@ class TimelineFragment :
} }
} }
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
if (isSwipeToRefreshEnabled) {
menuInflater.inflate(R.menu.fragment_timeline, menu)
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)
}
}
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_refresh -> {
if (isSwipeToRefreshEnabled) {
binding.swipeRefreshLayout.isRefreshing = true
refreshContent()
true
} else {
false
}
}
else -> false
}
}
/** /**
* Set the correct reading position in the timeline after the user clicked "Load more", * Set the correct reading position in the timeline after the user clicked "Load more",
* assuming the reading position should be below the freshly-loaded statuses. * assuming the reading position should be below the freshly-loaded statuses.

View file

@ -21,18 +21,28 @@ import android.os.Bundle
import androidx.fragment.app.commit import androidx.fragment.app.commit
import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ActivityViewThreadBinding
import com.keylesspalace.tusky.util.viewBinding
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import javax.inject.Inject import javax.inject.Inject
class ViewThreadActivity : BottomSheetActivity(), HasAndroidInjector { class ViewThreadActivity : BottomSheetActivity(), HasAndroidInjector {
private val binding by viewBinding(ActivityViewThreadBinding::inflate)
@Inject @Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any> lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_view_thread) setContentView(binding.root)
setSupportActionBar(binding.toolbar)
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
setDisplayShowTitleEnabled(true)
}
val id = intent.getStringExtra(ID_EXTRA)!! val id = intent.getStringExtra(ID_EXTRA)!!
val url = intent.getStringExtra(URL_EXTRA)!! val url = intent.getStringExtra(URL_EXTRA)!!
val fragment = val fragment =

View file

@ -18,10 +18,14 @@ package com.keylesspalace.tusky.components.viewthread
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.annotation.CheckResult import androidx.annotation.CheckResult
import androidx.core.view.MenuProvider
import androidx.fragment.app.commit import androidx.fragment.app.commit
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -59,7 +63,12 @@ import kotlinx.coroutines.launch
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener, Injectable { class ViewThreadFragment :
SFragment(),
OnRefreshListener,
StatusActionListener,
MenuProvider,
Injectable {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
@ -74,6 +83,16 @@ class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener,
private var alwaysShowSensitiveMedia = false private var alwaysShowSensitiveMedia = false
private var alwaysOpenSpoiler = false private var alwaysOpenSpoiler = false
/**
* State of the "reveal" menu item that shows/hides content that is behind a content
* warning. Setting this invalidates the menu to redraw the menu item.
*/
private var revealButtonState = RevealButtonState.NO_BUTTON
set(value) {
field = value
requireActivity().invalidateMenu()
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
thisThreadsStatusId = requireArguments().getString(ID_EXTRA)!! thisThreadsStatusId = requireArguments().getString(ID_EXTRA)!!
@ -107,24 +126,7 @@ class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener,
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
binding.toolbar.setNavigationOnClickListener {
activity?.onBackPressedDispatcher?.onBackPressed()
}
binding.toolbar.inflateMenu(R.menu.view_thread_toolbar)
binding.toolbar.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.action_reveal -> {
viewModel.toggleRevealButton()
true
}
R.id.action_open_in_web -> {
context?.openLink(requireArguments().getString(URL_EXTRA)!!)
true
}
else -> false
}
}
binding.swipeRefreshLayout.setOnRefreshListener(this) binding.swipeRefreshLayout.setOnRefreshListener(this)
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
@ -154,7 +156,7 @@ class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener,
viewModel.uiState.collect { uiState -> viewModel.uiState.collect { uiState ->
when (uiState) { when (uiState) {
is ThreadUiState.Loading -> { is ThreadUiState.Loading -> {
updateRevealButton(RevealButtonState.NO_BUTTON) revealButtonState = RevealButtonState.NO_BUTTON
binding.recyclerView.hide() binding.recyclerView.hide()
binding.statusView.hide() binding.statusView.hide()
@ -175,7 +177,7 @@ class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener,
adapter.submitList(listOf(uiState.statusViewDatum)) adapter.submitList(listOf(uiState.statusViewDatum))
updateRevealButton(uiState.revealButton) revealButtonState = uiState.revealButton
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
binding.recyclerView.show() binding.recyclerView.show()
@ -186,18 +188,24 @@ class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener,
initialProgressBar.cancel() initialProgressBar.cancel()
threadProgressBar.cancel() threadProgressBar.cancel()
updateRevealButton(RevealButtonState.NO_BUTTON) revealButtonState = RevealButtonState.NO_BUTTON
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
binding.recyclerView.hide() binding.recyclerView.hide()
binding.statusView.show() binding.statusView.show()
if (uiState.throwable is IOException) { if (uiState.throwable is IOException) {
binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) { binding.statusView.setup(
R.drawable.elephant_offline,
R.string.error_network
) {
viewModel.retry(thisThreadsStatusId) viewModel.retry(thisThreadsStatusId)
} }
} else { } else {
binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) { binding.statusView.setup(
R.drawable.elephant_error,
R.string.error_generic
) {
viewModel.retry(thisThreadsStatusId) viewModel.retry(thisThreadsStatusId)
} }
} }
@ -216,11 +224,14 @@ class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener,
viewModel.isInitialLoad = false viewModel.isInitialLoad = false
// Ensure the top of the status is visible // Ensure the top of the status is visible
(binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(uiState.detailedStatusPosition, 0) (binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(
uiState.detailedStatusPosition,
0
)
} }
} }
updateRevealButton(uiState.revealButton) revealButtonState = uiState.revealButton
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
binding.recyclerView.show() binding.recyclerView.show()
@ -247,6 +258,41 @@ class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener,
viewModel.loadThread(thisThreadsStatusId) viewModel.loadThread(thisThreadsStatusId)
} }
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.fragment_view_thread, menu)
val actionReveal = menu.findItem(R.id.action_reveal)
actionReveal.isVisible = revealButtonState != RevealButtonState.NO_BUTTON
actionReveal.setIcon(
when (revealButtonState) {
RevealButtonState.REVEAL -> R.drawable.ic_eye_24dp
else -> R.drawable.ic_hide_media_24dp
}
)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_reveal -> {
viewModel.toggleRevealButton()
true
}
R.id.action_open_in_web -> {
context?.openLink(requireArguments().getString(URL_EXTRA)!!)
true
}
R.id.action_refresh -> {
onRefresh()
true
}
else -> false
}
}
override fun onResume() {
super.onResume()
requireActivity().title = getString(R.string.title_view_thread)
}
/** /**
* Create a job to implement a delayed-visible progress bar. * Create a job to implement a delayed-visible progress bar.
* *
@ -269,13 +315,6 @@ class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener,
} }
} }
private fun updateRevealButton(state: RevealButtonState) {
val menuItem = binding.toolbar.menu.findItem(R.id.action_reveal)
menuItem.isVisible = state != RevealButtonState.NO_BUTTON
menuItem.setIcon(if (state == RevealButtonState.REVEAL) R.drawable.ic_eye_24dp else R.drawable.ic_hide_media_24dp)
}
override fun onRefresh() { override fun onRefresh() {
viewModel.refresh(thisThreadsStatusId) viewModel.refresh(thisThreadsStatusId)
} }

View file

@ -17,15 +17,22 @@ package com.keylesspalace.tusky.components.viewthread.edits
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.StatusListActivity import com.keylesspalace.tusky.StatusListActivity
@ -38,11 +45,20 @@ import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
class ViewEditsFragment : Fragment(R.layout.fragment_view_thread), LinkListener, Injectable { class ViewEditsFragment :
Fragment(R.layout.fragment_view_thread),
LinkListener,
OnRefreshListener,
MenuProvider,
Injectable {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
@ -54,12 +70,10 @@ class ViewEditsFragment : Fragment(R.layout.fragment_view_thread), LinkListener,
private lateinit var statusId: String private lateinit var statusId: String
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
binding.toolbar.setNavigationOnClickListener { binding.swipeRefreshLayout.setOnRefreshListener(this)
activity?.onBackPressedDispatcher?.onBackPressed() binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
}
binding.toolbar.title = getString(R.string.title_edits)
binding.swipeRefreshLayout.isEnabled = false
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(context) binding.recyclerView.layoutManager = LinearLayoutManager(context)
@ -84,9 +98,11 @@ class ViewEditsFragment : Fragment(R.layout.fragment_view_thread), LinkListener,
binding.statusView.hide() binding.statusView.hide()
binding.initialProgressBar.show() binding.initialProgressBar.show()
} }
EditsUiState.Refreshing -> {}
is EditsUiState.Error -> { is EditsUiState.Error -> {
Log.w(TAG, "failed to load edits", uiState.throwable) Log.w(TAG, "failed to load edits", uiState.throwable)
binding.swipeRefreshLayout.isRefreshing = false
binding.recyclerView.hide() binding.recyclerView.hide()
binding.statusView.show() binding.statusView.show()
binding.initialProgressBar.hide() binding.initialProgressBar.hide()
@ -102,6 +118,7 @@ class ViewEditsFragment : Fragment(R.layout.fragment_view_thread), LinkListener,
} }
} }
is EditsUiState.Success -> { is EditsUiState.Success -> {
binding.swipeRefreshLayout.isRefreshing = false
binding.recyclerView.show() binding.recyclerView.show()
binding.statusView.hide() binding.statusView.hide()
binding.initialProgressBar.hide() binding.initialProgressBar.hide()
@ -121,6 +138,36 @@ class ViewEditsFragment : Fragment(R.layout.fragment_view_thread), LinkListener,
viewModel.loadEdits(statusId) viewModel.loadEdits(statusId)
} }
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.fragment_view_edits, menu)
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)
}
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_refresh -> {
binding.swipeRefreshLayout.isRefreshing = true
onRefresh()
true
}
else -> false
}
}
override fun onResume() {
super.onResume()
requireActivity().title = getString(R.string.title_edits)
}
override fun onRefresh() {
viewModel.loadEdits(statusId, force = true, refreshing = true)
}
override fun onViewAccount(id: String) { override fun onViewAccount(id: String) {
bottomSheetActivity?.startActivityWithSlideInAnimation(AccountActivity.getIntent(requireContext(), id)) bottomSheetActivity?.startActivityWithSlideInAnimation(AccountActivity.getIntent(requireContext(), id))
} }

View file

@ -20,8 +20,9 @@ import androidx.lifecycle.viewModelScope
import at.connyduck.calladapter.networkresult.fold import at.connyduck.calladapter.networkresult.fold
import com.keylesspalace.tusky.entity.StatusEdit import com.keylesspalace.tusky.entity.StatusEdit
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -30,25 +31,27 @@ class ViewEditsViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
private val _uiState: MutableStateFlow<EditsUiState> = MutableStateFlow(EditsUiState.Initial) private val _uiState: MutableStateFlow<EditsUiState> = MutableStateFlow(EditsUiState.Initial)
val uiState: Flow<EditsUiState> val uiState: StateFlow<EditsUiState> = _uiState.asStateFlow()
get() = _uiState
fun loadEdits(statusId: String, force: Boolean = false, refreshing: Boolean = false) { fun loadEdits(statusId: String, force: Boolean = false, refreshing: Boolean = false) {
if (force || _uiState.value is EditsUiState.Initial) { if (!force && _uiState.value !is EditsUiState.Initial) return
if (!refreshing) {
_uiState.value = EditsUiState.Loading if (refreshing) {
} _uiState.value = EditsUiState.Refreshing
viewModelScope.launch { } else {
api.statusEdits(statusId).fold( _uiState.value = EditsUiState.Loading
{ edits -> }
val sortedEdits = edits.sortedBy { edit -> edit.createdAt }.reversed()
_uiState.value = EditsUiState.Success(sortedEdits) viewModelScope.launch {
}, api.statusEdits(statusId).fold(
{ throwable -> { edits ->
_uiState.value = EditsUiState.Error(throwable) val sortedEdits = edits.sortedBy { edit -> edit.createdAt }.reversed()
} _uiState.value = EditsUiState.Success(sortedEdits)
) },
} { throwable ->
_uiState.value = EditsUiState.Error(throwable)
}
)
} }
} }
} }
@ -56,6 +59,10 @@ class ViewEditsViewModel @Inject constructor(
sealed interface EditsUiState { sealed interface EditsUiState {
object Initial : EditsUiState object Initial : EditsUiState
object Loading : EditsUiState object Loading : EditsUiState
// "Refreshing" state is necessary, otherwise a refresh state transition is Success -> Success,
// and state flows don't emit repeated states, so the UI never updates.
object Refreshing : EditsUiState
class Error(val throwable: Throwable) : EditsUiState class Error(val throwable: Throwable) : EditsUiState
data class Success( data class Success(
val edits: List<StatusEdit> val edits: List<StatusEdit>

View file

@ -27,6 +27,9 @@ import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.util.SparseBooleanArray; import android.util.SparseBooleanArray;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
@ -39,6 +42,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.arch.core.util.Function; import androidx.arch.core.util.Function;
import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.util.Pair; import androidx.core.util.Pair;
import androidx.core.view.MenuProvider;
import androidx.lifecycle.Lifecycle; import androidx.lifecycle.Lifecycle;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.AsyncDifferConfig; import androidx.recyclerview.widget.AsyncDifferConfig;
@ -120,7 +124,9 @@ public class NotificationsFragment extends SFragment implements
StatusActionListener, StatusActionListener,
NotificationsAdapter.NotificationActionListener, NotificationsAdapter.NotificationActionListener,
AccountActionListener, AccountActionListener,
Injectable, ReselectableFragment { Injectable,
MenuProvider,
ReselectableFragment {
private static final String TAG = "NotificationF"; // logging tag private static final String TAG = "NotificationF"; // logging tag
private static final int LOAD_AT_ONCE = 30; private static final int LOAD_AT_ONCE = 30;
@ -205,6 +211,8 @@ public class NotificationsFragment extends SFragment implements
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) { @Nullable Bundle savedInstanceState) {
requireActivity().addMenuProvider(this, getViewLifecycleOwner(), Lifecycle.State.RESUMED);
binding = FragmentTimelineNotificationsBinding.inflate(inflater, container, false); binding = FragmentTimelineNotificationsBinding.inflate(inflater, container, false);
@NonNull Context context = inflater.getContext(); // from inflater to silence warning @NonNull Context context = inflater.getContext(); // from inflater to silence warning
@ -287,6 +295,22 @@ public class NotificationsFragment extends SFragment implements
binding = null; binding = null;
} }
@Override
public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
menuInflater.inflate(R.menu.fragment_notifications, menu);
}
@Override
public boolean onMenuItemSelected(@NonNull MenuItem menuItem) {
if (menuItem.getItemId() == R.id.action_refresh) {
binding.swipeRefreshLayout.setRefreshing(true);
onRefresh();
return true;
}
return false;
}
private void updateFilterVisibility() { private void updateFilterVisibility() {
CoordinatorLayout.LayoutParams params = CoordinatorLayout.LayoutParams params =
(CoordinatorLayout.LayoutParams) binding.swipeRefreshLayout.getLayoutParams(); (CoordinatorLayout.LayoutParams) binding.swipeRefreshLayout.getLayoutParams();

View file

@ -4,22 +4,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="@dimen/actionbar_elevation">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationContentDescription="@string/abc_action_bar_up_description"
app:navigationIcon="?attr/homeAsUpIndicator"
app:title="@string/title_view_thread" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout" android:id="@+id/swipeRefreshLayout"
android:layout_width="640dp" android:layout_width="640dp"

View file

@ -6,6 +6,23 @@
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context="com.keylesspalace.tusky.components.viewthread.ViewThreadActivity"> tools:context="com.keylesspalace.tusky.components.viewthread.ViewThreadActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="@dimen/actionbar_elevation"
app:elevationOverlayEnabled="false">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
style="@style/Widget.AppCompat.Toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentInsetStartWithNavigation="0dp"
app:layout_scrollFlags="scroll|enterAlways"
app:navigationContentDescription="@string/action_open_drawer" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container" android:id="@+id/fragment_container"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -14,4 +31,4 @@
<include layout="@layout/item_status_bottom_sheet"/> <include layout="@layout/item_status_bottom_sheet"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -4,22 +4,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="@dimen/actionbar_elevation">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationContentDescription="@string/abc_action_bar_up_description"
app:navigationIcon="?attr/homeAsUpIndicator"
app:title="@string/title_view_thread" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout" android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -43,6 +43,11 @@
android:title="@string/action_hide_reblogs" android:title="@string/action_hide_reblogs"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/action_refresh"
android:title="@string/action_refresh"
app:showAsAction="never" />
<item <item
android:id="@+id/action_report" android:id="@+id/action_report"
android:title="@string/action_report" /> android:title="@string/action_report" />

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_refresh"
android:title="@string/action_refresh"
app:showAsAction="never" />
</menu>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:title="@string/action_search"
app:showAsAction="ifRoom" />
</menu>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_refresh"
android:title="@string/action_refresh"
app:showAsAction="never" />
</menu>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_refresh"
android:title="@string/action_refresh"
app:showAsAction="never" />
</menu>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_refresh"
android:title="@string/action_refresh"
app:showAsAction="never" />
</menu>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_refresh"
android:title="@string/action_refresh"
app:showAsAction="never" />
</menu>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_refresh"
android:title="@string/action_refresh"
app:showAsAction="never" />
</menu>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_refresh"
android:title="@string/action_refresh"
app:showAsAction="never" />
</menu>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_refresh"
android:title="@string/action_refresh"
app:showAsAction="never" />
</menu>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_refresh"
android:title="@string/action_refresh"
app:showAsAction="never" />
</menu>

View file

@ -13,5 +13,8 @@
app:showAsAction="ifRoom" app:showAsAction="ifRoom"
android:icon="@drawable/ic_eye_24dp" /> android:icon="@drawable/ic_eye_24dp" />
<item
</menu> android:id="@+id/action_refresh"
android:title="@string/action_refresh"
app:showAsAction="never" />
</menu>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
android:id="@+id/action_search" android:id="@+id/action_search"
android:title="@string/action_search" android:title="@string/action_search"
@ -8,4 +9,4 @@
app:actionViewClass="androidx.appcompat.widget.SearchView" app:actionViewClass="androidx.appcompat.widget.SearchView"
android:actionLayout="@layout/search_view" android:actionLayout="@layout/search_view"
app:showAsAction="always" /> app:showAsAction="always" />
</menu> </menu>

View file

@ -730,6 +730,7 @@
<string name="report_category_other">Other</string> <string name="report_category_other">Other</string>
<string name="action_unfollow_hashtag_format">Unfollow #%s?</string> <string name="action_unfollow_hashtag_format">Unfollow #%s?</string>
<string name="action_refresh">Refresh</string>
<string name="mute_notifications_switch">Mute notifications</string> <string name="mute_notifications_switch">Mute notifications</string>
<!-- Reading order preference --> <!-- Reading order preference -->