diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 7a19149f..6b8978de 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -29,6 +29,8 @@ import android.net.Uri import android.os.Bundle import android.util.Log import android.view.KeyEvent +import android.view.Menu +import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.widget.ImageView @@ -40,6 +42,7 @@ import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.view.GravityCompat +import androidx.core.view.MenuProvider import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager @@ -135,7 +138,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers import kotlinx.coroutines.launch import javax.inject.Inject -class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector { +class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector, MenuProvider { @Inject lateinit var androidInjector: DispatchingAndroidInjector @@ -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 setContentView(binding.root) + setSupportActionBar(binding.mainToolbar) glide = Glide.with(this) @@ -257,17 +261,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje loadDrawerAvatar(activeAccount.profilePictureUrl, true) - binding.mainToolbar.menu.add(R.string.action_search).apply { - 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 - } - } + addMenuProvider(this) binding.viewPager.reduceSwipeSensitivity() @@ -352,6 +346,26 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje 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() { super.onResume() val currentEmojiPack = preferences.getString(EMOJI_PREFERENCE, "") @@ -745,7 +759,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } 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 { (tabAdapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect() } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt index ba261c1d..1b080020 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt @@ -26,6 +26,7 @@ import android.graphics.drawable.LayerDrawable import android.os.Bundle import android.text.Editable import android.view.Menu +import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup @@ -35,6 +36,7 @@ import androidx.annotation.Px import androidx.appcompat.app.AlertDialog import androidx.appcompat.content.res.AppCompatResources import androidx.core.app.ActivityOptionsCompat +import androidx.core.view.MenuProvider import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat 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.visible 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.HasAndroidInjector import java.text.NumberFormat @@ -97,7 +103,7 @@ import java.util.Locale import javax.inject.Inject import kotlin.math.abs -class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector, LinkListener { +class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider, HasAndroidInjector, LinkListener { @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector @@ -153,6 +159,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI loadResources() makeNotificationBarTransparent() setContentView(binding.root) + addMenuProvider(this) // Obtain information to fill out the profile. viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!) @@ -414,14 +421,16 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI draftsAlert.observeInContext(this, true) } + private fun onRefresh() { + viewModel.refresh() + adapter.refreshContent() + } + /** * Setup swipe to refresh layout */ private fun setupRefreshLayout() { - binding.swipeToRefreshLayout.setOnRefreshListener { - viewModel.refresh() - adapter.refreshContent() - } + binding.swipeToRefreshLayout.setOnRefreshListener { onRefresh() } viewModel.isRefreshing.observe( this ) { 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) 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) } - 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() { @@ -884,7 +898,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI viewUrl(url) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { + override fun onMenuItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_open_in_web -> { // If the account isn't loaded yet, eat the input. @@ -949,6 +963,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI viewModel.changeShowReblogsState() return true } + R.id.action_refresh -> { + binding.swipeToRefreshLayout.isRefreshing = true + onRefresh() + return true + } R.id.action_report -> { loadedAccount?.let { loadedAccount -> startActivity(ReportActivity.getIntent(this, viewModel.accountId, loadedAccount.username)) @@ -956,7 +975,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI return true } } - return super.onOptionsItemSelected(item) + return false } override fun getActionButton(): FloatingActionButton? { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt index 457cda7b..df87372e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt @@ -16,15 +16,21 @@ package com.keylesspalace.tusky.components.account.media import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.View import androidx.core.app.ActivityOptionsCompat +import androidx.core.view.MenuProvider import androidx.core.view.ViewCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.paging.LoadState import androidx.preference.PreferenceManager import androidx.recyclerview.widget.GridLayoutManager +import com.google.android.material.color.MaterialColors import com.keylesspalace.tusky.R import com.keylesspalace.tusky.ViewMediaActivity 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.viewBinding 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.launch import java.io.IOException import javax.inject.Inject /** - * Created by charlag on 26/10/2017. - * * Fragment with multiple columns of media previews for the specified account. */ - class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFragment, + MenuProvider, Injectable { @Inject @@ -73,6 +81,7 @@ class AccountMediaFragment : } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) val alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia @@ -95,6 +104,8 @@ class AccountMediaFragment : binding.recyclerView.adapter = adapter binding.swipeRefreshLayout.isEnabled = false + binding.swipeRefreshLayout.setOnRefreshListener { refreshContent() } + binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) binding.statusView.visibility = View.GONE @@ -108,6 +119,10 @@ class AccountMediaFragment : binding.statusView.hide() binding.progressBar.hide() + if (loadState.refresh != LoadState.Loading && loadState.source.refresh != LoadState.Loading) { + binding.swipeRefreshLayout.isRefreshing = false + } + if (adapter.itemCount == 0) { when (loadState.refresh) { 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) { if (!selected.isRevealed) { viewModel.revealAttachment(selected) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt index 14fbcf8c..a62fb12b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt @@ -19,12 +19,17 @@ import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.View import android.widget.PopupWindow import androidx.activity.viewModels +import androidx.core.view.MenuProvider import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.color.MaterialColors import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.R 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.viewBinding 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 -class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, OnEmojiSelectedListener, Injectable { +class AnnouncementsActivity : + BottomSheetActivity(), + AnnouncementActionListener, + OnEmojiSelectedListener, + MenuProvider, + Injectable { @Inject lateinit var viewModelFactory: ViewModelFactory @@ -71,6 +85,7 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) + addMenuProvider(this) setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.apply { @@ -130,6 +145,27 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, 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() { viewModel.load() binding.swipeRefreshLayout.isRefreshing = true diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index b05df2f8..fc5cebe6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -17,10 +17,14 @@ package com.keylesspalace.tusky.components.conversation import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.PopupMenu +import androidx.core.view.MenuProvider import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope @@ -32,6 +36,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator import at.connyduck.sparkbutton.helpers.Utils import autodispose2.androidx.lifecycle.autoDispose +import com.google.android.material.color.MaterialColors import com.keylesspalace.tusky.R import com.keylesspalace.tusky.StatusListActivity 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.viewBinding 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 kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest @@ -61,7 +70,12 @@ import javax.inject.Inject import kotlin.time.DurationUnit import kotlin.time.toDuration -class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment { +class ConversationsFragment : + SFragment(), + StatusActionListener, + Injectable, + ReselectableFragment, + MenuProvider { @Inject lateinit var viewModelFactory: ViewModelFactory @@ -82,6 +96,8 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) + val preferences = PreferenceManager.getDefaultSharedPreferences(view.context) 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() { binding.recyclerView.setHasFixedSize(true) binding.recyclerView.layoutManager = LinearLayoutManager(context) @@ -200,10 +237,12 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res binding.recyclerView.adapter = adapter.withLoadStateFooter(ConversationLoadStateAdapter(adapter::retry)) } + private fun refreshContent() { + adapter.refresh() + } + private fun initSwipeToRefresh() { - binding.swipeRefreshLayout.setOnRefreshListener { - adapter.refresh() - } + binding.swipeRefreshLayout.setOnRefreshListener { refreshContent() } binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt index 7210fbef..f65e29c3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt @@ -16,17 +16,24 @@ package com.keylesspalace.tusky.components.report.fragments import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.View import androidx.core.app.ActivityOptionsCompat +import androidx.core.view.MenuProvider import androidx.core.view.ViewCompat import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.paging.LoadState import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager 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.keylesspalace.tusky.R 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.visible 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.launch 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 lateinit var viewModelFactory: ViewModelFactory @@ -90,18 +106,42 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) handleClicks() initStatusesView() 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() { binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) - binding.swipeRefreshLayout.setOnRefreshListener { - snackbarErrorRetry?.dismiss() - adapter.refresh() - } + binding.swipeRefreshLayout.setOnRefreshListener(this) } private fun initStatusesView() { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusActivity.kt index ade93322..d78bc683 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusActivity.kt @@ -18,13 +18,18 @@ package com.keylesspalace.tusky.components.scheduled import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog +import androidx.core.view.MenuProvider import androidx.lifecycle.lifecycleScope import androidx.paging.LoadState import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import autodispose2.androidx.lifecycle.autoDispose +import com.google.android.material.color.MaterialColors import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R 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.util.hide 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 kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import javax.inject.Inject -class ScheduledStatusActivity : BaseActivity(), ScheduledStatusActionListener, Injectable { +class ScheduledStatusActivity : + BaseActivity(), + ScheduledStatusActionListener, + MenuProvider, + Injectable { @Inject lateinit var viewModelFactory: ViewModelFactory @@ -51,13 +65,15 @@ class ScheduledStatusActivity : BaseActivity(), ScheduledStatusActionListener, I private val viewModel: ScheduledStatusViewModel by viewModels { viewModelFactory } + private val binding by viewBinding(ActivityScheduledStatusBinding::inflate) + private val adapter = ScheduledStatusAdapter(this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val binding = ActivityScheduledStatusBinding.inflate(layoutInflater) setContentView(binding.root) + addMenuProvider(this) setSupportActionBar(binding.includedToolbar.toolbar) 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() { adapter.refresh() } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt index beb31611..90bf04e3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt @@ -20,8 +20,11 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import androidx.activity.viewModels import androidx.appcompat.widget.SearchView +import androidx.core.view.MenuProvider import androidx.preference.PreferenceManager import com.google.android.material.tabs.TabLayoutMediator import com.keylesspalace.tusky.BottomSheetActivity @@ -37,7 +40,7 @@ import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import javax.inject.Inject -class SearchActivity : BottomSheetActivity(), HasAndroidInjector { +class SearchActivity : BottomSheetActivity(), HasAndroidInjector, MenuProvider { @Inject lateinit var androidInjector: DispatchingAndroidInjector @@ -59,6 +62,7 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector { setDisplayShowHomeEnabled(true) setDisplayShowTitleEnabled(false) } + addMenuProvider(this) setupPages() handleIntent(intent) } @@ -81,17 +85,18 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector { handleIntent(intent) } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - super.onCreateOptionsMenu(menu) - + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.search_toolbar, menu) - val searchView = menu.findItem(R.id.action_search) - .actionView as SearchView + val searchViewMenuItem = menu.findItem(R.id.action_search) + searchViewMenuItem.expandActionView() + val searchView = searchViewMenuItem.actionView as SearchView setupSearchView(searchView) searchView.setQuery(viewModel.currentQuery, false) + } - return true + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return false } override fun finish() { @@ -116,17 +121,42 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector { private fun setupSearchView(searchView: SearchView) { searchView.setIconifiedByDefault(false) - 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 companion object { + const val TAG = "SearchActivity" fun getIntent(context: Context) = Intent(context, SearchActivity::class.java) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt index 3fe818cb..86b54238 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt @@ -1,9 +1,14 @@ package com.keylesspalace.tusky.components.search.fragments import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.View +import androidx.core.view.MenuProvider import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.paging.LoadState import androidx.paging.PagingData @@ -12,6 +17,7 @@ import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.SimpleItemAnimator import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.google.android.material.color.MaterialColors import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.BottomSheetActivity 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.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 +import com.mikepenz.iconics.utils.sizeDp import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -34,7 +44,8 @@ abstract class SearchFragment : Fragment(R.layout.fragment_search), LinkListener, Injectable, - SwipeRefreshLayout.OnRefreshListener { + SwipeRefreshLayout.OnRefreshListener, + MenuProvider { @Inject lateinit var viewModelFactory: ViewModelFactory @@ -58,6 +69,7 @@ abstract class SearchFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { initAdapter() setupSwipeRefreshLayout() + requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) subscribeObservables() } @@ -95,6 +107,27 @@ abstract class SearchFragment : } } + 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() { binding.searchRecyclerView.addItemDecoration(DividerItemDecoration(binding.searchRecyclerView.context, DividerItemDecoration.VERTICAL)) binding.searchRecyclerView.layoutManager = LinearLayoutManager(binding.searchRecyclerView.context) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt index fe7b5d14..36d20e68 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt @@ -18,10 +18,14 @@ package com.keylesspalace.tusky.components.timeline import android.os.Bundle import android.util.Log import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.view.accessibility.AccessibilityManager import androidx.core.content.ContextCompat +import androidx.core.view.MenuProvider import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope @@ -34,6 +38,7 @@ import androidx.recyclerview.widget.SimpleItemAnimator import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import at.connyduck.sparkbutton.helpers.Utils import autodispose2.androidx.lifecycle.autoDispose +import com.google.android.material.color.MaterialColors import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R 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.viewdata.AttachmentViewData 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.core.Observable import kotlinx.coroutines.flow.collectLatest @@ -79,7 +88,8 @@ class TimelineFragment : StatusActionListener, Injectable, ReselectableFragment, - RefreshableFragment { + RefreshableFragment, + MenuProvider { @Inject lateinit var viewModelFactory: ViewModelFactory @@ -198,6 +208,8 @@ class TimelineFragment : } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) + setupSwipeRefreshLayout() 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", * assuming the reading position should be below the freshly-loaded statuses. diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadActivity.kt index ed0393fa..70c0df19 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadActivity.kt @@ -21,18 +21,28 @@ import android.os.Bundle import androidx.fragment.app.commit import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ActivityViewThreadBinding +import com.keylesspalace.tusky.util.viewBinding import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import javax.inject.Inject class ViewThreadActivity : BottomSheetActivity(), HasAndroidInjector { + private val binding by viewBinding(ActivityViewThreadBinding::inflate) + @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector override fun onCreate(savedInstanceState: Bundle?) { 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 url = intent.getStringExtra(URL_EXTRA)!! val fragment = diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt index 84379e07..4baa0ff1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt @@ -18,10 +18,14 @@ package com.keylesspalace.tusky.components.viewthread import android.os.Bundle import android.util.Log import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.LinearLayout import androidx.annotation.CheckResult +import androidx.core.view.MenuProvider import androidx.fragment.app.commit import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle @@ -59,7 +63,12 @@ import kotlinx.coroutines.launch import java.io.IOException import javax.inject.Inject -class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener, Injectable { +class ViewThreadFragment : + SFragment(), + OnRefreshListener, + StatusActionListener, + MenuProvider, + Injectable { @Inject lateinit var viewModelFactory: ViewModelFactory @@ -74,6 +83,16 @@ class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener, private var alwaysShowSensitiveMedia = 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?) { super.onCreate(savedInstanceState) thisThreadsStatusId = requireArguments().getString(ID_EXTRA)!! @@ -107,24 +126,7 @@ class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener, } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - - 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 - } - } + requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) binding.swipeRefreshLayout.setOnRefreshListener(this) binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) @@ -154,7 +156,7 @@ class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener, viewModel.uiState.collect { uiState -> when (uiState) { is ThreadUiState.Loading -> { - updateRevealButton(RevealButtonState.NO_BUTTON) + revealButtonState = RevealButtonState.NO_BUTTON binding.recyclerView.hide() binding.statusView.hide() @@ -175,7 +177,7 @@ class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener, adapter.submitList(listOf(uiState.statusViewDatum)) - updateRevealButton(uiState.revealButton) + revealButtonState = uiState.revealButton binding.swipeRefreshLayout.isRefreshing = false binding.recyclerView.show() @@ -186,18 +188,24 @@ class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener, initialProgressBar.cancel() threadProgressBar.cancel() - updateRevealButton(RevealButtonState.NO_BUTTON) + revealButtonState = RevealButtonState.NO_BUTTON binding.swipeRefreshLayout.isRefreshing = false binding.recyclerView.hide() binding.statusView.show() 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) } } 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) } } @@ -216,11 +224,14 @@ class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener, viewModel.isInitialLoad = false // 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.recyclerView.show() @@ -247,6 +258,41 @@ class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener, 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. * @@ -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() { viewModel.refresh(thisThreadsStatusId) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsFragment.kt index d02d017d..f829141a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsFragment.kt @@ -17,15 +17,22 @@ package com.keylesspalace.tusky.components.viewthread.edits import android.os.Bundle import android.util.Log +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.View import android.widget.LinearLayout +import androidx.core.view.MenuProvider import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager 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.R 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.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 kotlinx.coroutines.launch import java.io.IOException 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 lateinit var viewModelFactory: ViewModelFactory @@ -54,12 +70,10 @@ class ViewEditsFragment : Fragment(R.layout.fragment_view_thread), LinkListener, private lateinit var statusId: String override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) - binding.toolbar.setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() - } - binding.toolbar.title = getString(R.string.title_edits) - binding.swipeRefreshLayout.isEnabled = false + binding.swipeRefreshLayout.setOnRefreshListener(this) + binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) binding.recyclerView.setHasFixedSize(true) binding.recyclerView.layoutManager = LinearLayoutManager(context) @@ -84,9 +98,11 @@ class ViewEditsFragment : Fragment(R.layout.fragment_view_thread), LinkListener, binding.statusView.hide() binding.initialProgressBar.show() } + EditsUiState.Refreshing -> {} is EditsUiState.Error -> { Log.w(TAG, "failed to load edits", uiState.throwable) + binding.swipeRefreshLayout.isRefreshing = false binding.recyclerView.hide() binding.statusView.show() binding.initialProgressBar.hide() @@ -102,6 +118,7 @@ class ViewEditsFragment : Fragment(R.layout.fragment_view_thread), LinkListener, } } is EditsUiState.Success -> { + binding.swipeRefreshLayout.isRefreshing = false binding.recyclerView.show() binding.statusView.hide() binding.initialProgressBar.hide() @@ -121,6 +138,36 @@ class ViewEditsFragment : Fragment(R.layout.fragment_view_thread), LinkListener, 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) { bottomSheetActivity?.startActivityWithSlideInAnimation(AccountActivity.getIntent(requireContext(), id)) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt index a76078ed..c5959d26 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt @@ -20,8 +20,9 @@ import androidx.lifecycle.viewModelScope import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.entity.StatusEdit import com.keylesspalace.tusky.network.MastodonApi -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -30,25 +31,27 @@ class ViewEditsViewModel @Inject constructor( ) : ViewModel() { private val _uiState: MutableStateFlow = MutableStateFlow(EditsUiState.Initial) - val uiState: Flow - get() = _uiState + val uiState: StateFlow = _uiState.asStateFlow() fun loadEdits(statusId: String, force: Boolean = false, refreshing: Boolean = false) { - if (force || _uiState.value is EditsUiState.Initial) { - if (!refreshing) { - _uiState.value = EditsUiState.Loading - } - viewModelScope.launch { - api.statusEdits(statusId).fold( - { edits -> - val sortedEdits = edits.sortedBy { edit -> edit.createdAt }.reversed() - _uiState.value = EditsUiState.Success(sortedEdits) - }, - { throwable -> - _uiState.value = EditsUiState.Error(throwable) - } - ) - } + if (!force && _uiState.value !is EditsUiState.Initial) return + + if (refreshing) { + _uiState.value = EditsUiState.Refreshing + } else { + _uiState.value = EditsUiState.Loading + } + + viewModelScope.launch { + api.statusEdits(statusId).fold( + { edits -> + 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 { object Initial : 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 data class Success( val edits: List diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 9c9d3c6d..24d026cc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -27,6 +27,9 @@ import android.os.Bundle; import android.util.Log; import android.util.SparseBooleanArray; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; @@ -39,6 +42,7 @@ import androidx.appcompat.app.AlertDialog; import androidx.arch.core.util.Function; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.util.Pair; +import androidx.core.view.MenuProvider; import androidx.lifecycle.Lifecycle; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.AsyncDifferConfig; @@ -120,7 +124,9 @@ public class NotificationsFragment extends SFragment implements StatusActionListener, NotificationsAdapter.NotificationActionListener, AccountActionListener, - Injectable, ReselectableFragment { + Injectable, + MenuProvider, + ReselectableFragment { private static final String TAG = "NotificationF"; // logging tag private static final int LOAD_AT_ONCE = 30; @@ -205,6 +211,8 @@ public class NotificationsFragment extends SFragment implements @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + requireActivity().addMenuProvider(this, getViewLifecycleOwner(), Lifecycle.State.RESUMED); + binding = FragmentTimelineNotificationsBinding.inflate(inflater, container, false); @NonNull Context context = inflater.getContext(); // from inflater to silence warning @@ -287,6 +295,22 @@ public class NotificationsFragment extends SFragment implements 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() { CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) binding.swipeRefreshLayout.getLayoutParams(); diff --git a/app/src/main/res/layout-sw640dp/fragment_view_thread.xml b/app/src/main/res/layout-sw640dp/fragment_view_thread.xml index 9248c542..e11b8c25 100644 --- a/app/src/main/res/layout-sw640dp/fragment_view_thread.xml +++ b/app/src/main/res/layout-sw640dp/fragment_view_thread.xml @@ -4,22 +4,6 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - - - - - + + + + + - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_view_thread.xml b/app/src/main/res/layout/fragment_view_thread.xml index f0f348c5..539efe57 100644 --- a/app/src/main/res/layout/fragment_view_thread.xml +++ b/app/src/main/res/layout/fragment_view_thread.xml @@ -4,22 +4,6 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - - - - - + + diff --git a/app/src/main/res/menu/activity_announcements.xml b/app/src/main/res/menu/activity_announcements.xml new file mode 100644 index 00000000..bf722917 --- /dev/null +++ b/app/src/main/res/menu/activity_announcements.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/menu/activity_main.xml b/app/src/main/res/menu/activity_main.xml new file mode 100644 index 00000000..a1ca8b75 --- /dev/null +++ b/app/src/main/res/menu/activity_main.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/activity_scheduled_status.xml b/app/src/main/res/menu/activity_scheduled_status.xml new file mode 100644 index 00000000..bf722917 --- /dev/null +++ b/app/src/main/res/menu/activity_scheduled_status.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/menu/fragment_account_media.xml b/app/src/main/res/menu/fragment_account_media.xml new file mode 100644 index 00000000..bf722917 --- /dev/null +++ b/app/src/main/res/menu/fragment_account_media.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/menu/fragment_conversations.xml b/app/src/main/res/menu/fragment_conversations.xml new file mode 100644 index 00000000..bf722917 --- /dev/null +++ b/app/src/main/res/menu/fragment_conversations.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/menu/fragment_notifications.xml b/app/src/main/res/menu/fragment_notifications.xml new file mode 100644 index 00000000..bf722917 --- /dev/null +++ b/app/src/main/res/menu/fragment_notifications.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/menu/fragment_report_statuses.xml b/app/src/main/res/menu/fragment_report_statuses.xml new file mode 100644 index 00000000..bf722917 --- /dev/null +++ b/app/src/main/res/menu/fragment_report_statuses.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/menu/fragment_search.xml b/app/src/main/res/menu/fragment_search.xml new file mode 100644 index 00000000..bf722917 --- /dev/null +++ b/app/src/main/res/menu/fragment_search.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/menu/fragment_timeline.xml b/app/src/main/res/menu/fragment_timeline.xml new file mode 100644 index 00000000..bf722917 --- /dev/null +++ b/app/src/main/res/menu/fragment_timeline.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/menu/fragment_view_edits.xml b/app/src/main/res/menu/fragment_view_edits.xml new file mode 100644 index 00000000..bf722917 --- /dev/null +++ b/app/src/main/res/menu/fragment_view_edits.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/menu/view_thread_toolbar.xml b/app/src/main/res/menu/fragment_view_thread.xml similarity index 77% rename from app/src/main/res/menu/view_thread_toolbar.xml rename to app/src/main/res/menu/fragment_view_thread.xml index 65577d20..286ca56e 100644 --- a/app/src/main/res/menu/view_thread_toolbar.xml +++ b/app/src/main/res/menu/fragment_view_thread.xml @@ -13,5 +13,8 @@ app:showAsAction="ifRoom" android:icon="@drawable/ic_eye_24dp" /> - - \ No newline at end of file + + diff --git a/app/src/main/res/menu/search_toolbar.xml b/app/src/main/res/menu/search_toolbar.xml index 633ca7ef..6ac14511 100644 --- a/app/src/main/res/menu/search_toolbar.xml +++ b/app/src/main/res/menu/search_toolbar.xml @@ -1,6 +1,7 @@ + - \ No newline at end of file + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7a08a1ba..9ee06107 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -730,6 +730,7 @@ Other Unfollow #%s? + Refresh Mute notifications