feat: Simplify adding/removing timelines from tabs (#587)

Previously, modifying any tabs meant opening the left-side nav, opening
Account preferences > Tabs, and then adding / removing tabs. This is
time consuming, and difficult for new users to discover.

In addition, it was possible to remove the Home tab, and there was a
hardcoded minimum of at least two tabs.

Fix this.

When viewing a timeline that is not already in a tab an "Add to tab"
menu item is enabled, which appends the timeline to the list of existing
tabs.

When viewing a timeline in a tab (that is not the Home timeline) a
"Remove tab" menu item is enabled, which removes the tab from the list
of existing tabs.

If the user removes the active tab (either with this menu item, or
through preferences) the tab to the left of the active tab becomes the
new active tab.

A new "Manage tabs" menu item is also provided, as a shortcut to the
existing Account preferences > Tabs screen.

When managing tabs the Home timeline can not be removed; the button to
remove it is removed, and swiping is disabled on that list item. The
restriction of "at least two tabs" has also been removed.

`NotificationsActivity` has been removed, as `TimelineActivity` can
display `NotificationsFragment`.

To make the three "Trending" types (hashtags, links, and posts) more
visually distinct add two new icons for links (ic_newspaper) and posts
(ic_whatshot).

Fixes #572, #584, #585, #569
This commit is contained in:
Nik Clayton 2024-04-03 00:10:09 +02:00 committed by GitHub
parent 0829d91989
commit 6bef6f2fae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 270 additions and 306 deletions

View File

@ -949,21 +949,10 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/app/pachli/adapter/TabAdapter.kt"
line="55"
line="53"
column="9"/>
</issue>
<issue
id="NotifyDataSetChanged"
message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort."
errorLine1=" notifyDataSetChanged()"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/app/pachli/adapter/TabAdapter.kt"
line="158"
column="13"/>
</issue>
<issue
id="VectorPath"
message="Very long vector path (2783 characters), which is bad for performance. Considering reducing precision, removing minor details or rasterizing vector."
@ -2430,7 +2419,7 @@
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/item_tab_preference.xml"
line="26"
line="27"
column="6"/>
</issue>
@ -2621,28 +2610,6 @@
column="5"/>
</issue>
<issue
id="ClickableViewAccessibility"
message="Custom view ``ImageView`` has `setOnTouchListener` called on it but does not override `performClick`"
errorLine1=" binding.imageView.setOnTouchListener { _, event ->"
errorLine2=" ^">
<location
file="src/main/java/app/pachli/adapter/TabAdapter.kt"
line="92"
column="13"/>
</issue>
<issue
id="ClickableViewAccessibility"
message="`onTouch` lambda should call `View#performClick` when a click is detected"
errorLine1=" binding.imageView.setOnTouchListener { _, event ->"
errorLine2=" ^">
<location
file="src/main/java/app/pachli/adapter/TabAdapter.kt"
line="92"
column="50"/>
</issue>
<issue
id="ContentDescription"
message="Missing `contentDescription` attribute on image"

View File

@ -122,7 +122,6 @@
<activity
android:name=".components.account.AccountActivity"
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" />
<activity android:name=".components.notifications.NotificationsActivity" />
<activity android:name=".EditProfileActivity" />
<activity android:name=".components.preference.PreferencesActivity" />
<activity android:name=".TimelineActivity" />

View File

@ -94,11 +94,11 @@ import app.pachli.core.navigation.ListActivityIntent
import app.pachli.core.navigation.LoginActivityIntent
import app.pachli.core.navigation.LoginActivityIntent.LoginMode
import app.pachli.core.navigation.MainActivityIntent
import app.pachli.core.navigation.NotificationsActivityIntent
import app.pachli.core.navigation.PreferencesActivityIntent
import app.pachli.core.navigation.PreferencesActivityIntent.PreferenceScreen
import app.pachli.core.navigation.ScheduledStatusActivityIntent
import app.pachli.core.navigation.SearchActivityIntent
import app.pachli.core.navigation.TabPreferenceActivityIntent
import app.pachli.core.navigation.TimelineActivityIntent
import app.pachli.core.navigation.TrendingActivityIntent
import app.pachli.core.network.model.Account
@ -115,6 +115,7 @@ import app.pachli.updatecheck.UpdateCheck
import app.pachli.usecase.DeveloperToolsUseCase
import app.pachli.usecase.LogoutUsecase
import app.pachli.util.getDimension
import app.pachli.util.makeIcon
import app.pachli.util.updateShortcut
import at.connyduck.calladapter.networkresult.fold
import com.bumptech.glide.Glide
@ -131,10 +132,8 @@ import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import com.google.android.material.tabs.TabLayoutMediator
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.IconicsSize
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import com.mikepenz.materialdrawer.holder.BadgeStyle
import com.mikepenz.materialdrawer.holder.ColorHolder
import com.mikepenz.materialdrawer.holder.StringHolder
@ -163,7 +162,9 @@ import com.mikepenz.materialdrawer.widget.AccountHeaderView
import dagger.hilt.android.AndroidEntryPoint
import de.c1710.filemojicompat_ui.helpers.EMOJI_PREFERENCE
import javax.inject.Inject
import kotlin.math.max
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
@ -407,18 +408,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
super.onCreateMenu(menu, 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)
}
icon = makeIcon(this@MainActivity, GoogleMaterial.Icon.gmd_search, IconicsSize.dp(20))
}
}
override fun onPrepareMenu(menu: Menu) {
super<BottomSheetActivity>.onPrepareMenu(menu)
menu.findItem(R.id.action_remove_tab).isVisible = tabAdapter.tabs[binding.viewPager.currentItem].timeline != Timeline.Home
// If the main toolbar is hidden then there's no space in the top/bottomNav to show
// the menu items as icons, so forceably disable them
if (!binding.mainToolbar.isVisible) menu.forEach { it.setShowAsAction(SHOW_AS_ACTION_NEVER) }
@ -431,6 +432,21 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
startActivity(SearchActivityIntent(this@MainActivity))
true
}
R.id.action_remove_tab -> {
val timeline = tabAdapter.tabs[binding.viewPager.currentItem].timeline
accountManager.activeAccount?.let {
lifecycleScope.launch(Dispatchers.IO) {
it.tabPreferences = it.tabPreferences.filterNot { it == timeline }
accountManager.saveAccount(it)
eventHub.dispatch(MainTabsChangedEvent(it.tabPreferences))
}
}
true
}
R.id.action_tab_preferences -> {
startActivity(TabPreferenceActivityIntent(this))
true
}
else -> super.onOptionsItemSelected(menuItem)
}
}
@ -620,7 +636,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
iconicsIcon = GoogleMaterial.Icon.gmd_notifications
onClick = {
startActivityWithSlideInAnimation(
NotificationsActivityIntent(context),
TimelineActivityIntent.notifications(context),
)
}
},
@ -876,7 +892,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
}
// Save the previous tab so it can be restored later
val previousTab = tabAdapter.tabs.getOrNull(binding.viewPager.currentItem)
val previousTabIndex = binding.viewPager.currentItem
val previousTab = tabAdapter.tabs.getOrNull(previousTabIndex)
val tabs = accountManager.activeAccount!!.tabPreferences.map { TabViewData.from(it) }
@ -896,6 +913,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
// - Notification tab (if appropriate)
// - The previously selected tab (if it hasn't been removed)
// - Tabs containing lists are compared by list ID, in case the list was renamed
// - The tab to the left of the previous selected tab (if the previously selected tab
// was removed)
// - Left-most tab
val position = if (selectNotificationTab) {
tabs.indexOfFirst { it.timeline is Timeline.Notifications }
@ -909,7 +928,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
}
}
}
}.takeIf { it != -1 } ?: 0
}.takeIf { it != -1 } ?: max(previousTabIndex - 1, 0)
binding.viewPager.setCurrentItem(position, false)
val pageMargin = resources.getDimensionPixelSize(DR.dimen.tab_page_margin)

View File

@ -112,7 +112,7 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
}
currentTabs = accountManager.activeAccount?.tabPreferences.orEmpty().map { TabViewData.from(it) }.toMutableList()
currentTabsAdapter = TabAdapter(currentTabs, false, this, currentTabs.size <= MIN_TAB_COUNT)
currentTabsAdapter = TabAdapter(currentTabs, false, this)
binding.currentTabsRecyclerView.adapter = currentTabsAdapter
binding.currentTabsRecyclerView.layoutManager = LinearLayoutManager(this)
binding.currentTabsRecyclerView.addItemDecoration(
@ -129,12 +129,9 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.END)
}
override fun isLongPressDragEnabled(): Boolean {
return true
}
override fun isItemViewSwipeEnabled(): Boolean {
return MIN_TAB_COUNT < currentTabs.size
// Swiping enabled in TabAdapter.onBindViewHolder if the timeline is not Timeline.Home
return false
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
@ -394,8 +391,6 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
addableTabs.add(TabViewData.from(Timeline.UserList("", "")))
addTabAdapter.updateData(addableTabs)
currentTabsAdapter.setRemoveButtonVisible(currentTabs.size > MIN_TAB_COUNT)
}
override fun onStartDelete(viewHolder: RecyclerView.ViewHolder) {
@ -424,8 +419,4 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
}
}
}
companion object {
private const val MIN_TAB_COUNT = 2
}
}

View File

@ -102,7 +102,6 @@ data class TabViewData(
ComposeActivityIntent.ComposeOptions(visibility = Status.Visibility.PRIVATE),
)
}
Timeline.TrendingHashtags -> TabViewData(
timeline = timeline,
text = R.string.title_public_trending_hashtags,
@ -113,13 +112,13 @@ data class TabViewData(
Timeline.TrendingLinks -> TabViewData(
timeline = timeline,
text = R.string.title_public_trending_links,
icon = R.drawable.ic_trending_up_24px,
icon = R.drawable.ic_newspaper_24,
fragment = { TrendingLinksFragment.newInstance() },
)
Timeline.TrendingStatuses -> TabViewData(
timeline = timeline,
text = R.string.title_public_trending_statuses,
icon = R.drawable.ic_trending_up_24px,
icon = R.drawable.ic_whatshot_24,
fragment = { TimelineFragment.newInstance(timeline) },
)
is Timeline.Hashtags -> TabViewData(

View File

@ -19,11 +19,15 @@ package app.pachli
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.widget.Toast
import androidx.core.view.MenuProvider
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import app.pachli.appstore.EventHub
import app.pachli.appstore.FilterChangedEvent
import app.pachli.appstore.MainTabsChangedEvent
import app.pachli.core.activity.BottomSheetActivity
import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.common.util.unsafeLazy
@ -46,6 +50,7 @@ import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import io.github.z4kn4fein.semver.constraints.toConstraint
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.HttpException
import timber.log.Timber
@ -54,7 +59,7 @@ import timber.log.Timber
* Show a single timeline.
*/
@AndroidEntryPoint
class TimelineActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButtonActivity {
class TimelineActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButtonActivity, MenuProvider {
@Inject
lateinit var eventHub: EventHub
@ -89,15 +94,15 @@ class TimelineActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButtonAc
setContentView(binding.root)
setSupportActionBar(binding.includedToolbar.toolbar)
addMenuProvider(this)
timeline = TimelineActivityIntent.getTimeline(intent)
hashtag = (timeline as? Timeline.Hashtags)?.tags?.firstOrNull()
val viewData = TabViewData.from(timeline)
val title = viewData.title(this)
supportActionBar?.run {
setTitle(title)
title = viewData.title(this@TimelineActivity)
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
}
@ -116,9 +121,10 @@ class TimelineActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButtonAc
} ?: binding.composeButton.hide()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val tag = hashtag
if (timeline is Timeline.Hashtags && tag != null) {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.activity_timeline, menu)
hashtag?.let { tag ->
lifecycleScope.launch {
mastodonApi.tag(tag).fold(
{ tagEntity ->
@ -129,10 +135,6 @@ class TimelineActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButtonAc
unmuteTagItem = menu.findItem(R.id.action_unmute_hashtag)
followTagItem?.isVisible = tagEntity.following == false
unfollowTagItem?.isVisible = tagEntity.following == true
followTagItem?.setOnMenuItemClickListener { followTag() }
unfollowTagItem?.setOnMenuItemClickListener { unfollowTag() }
muteTagItem?.setOnMenuItemClickListener { muteTag() }
unmuteTagItem?.setOnMenuItemClickListener { unmuteTag() }
updateMuteTagMenuItems()
},
{
@ -142,7 +144,52 @@ class TimelineActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButtonAc
}
}
return super.onCreateOptionsMenu(menu)
return super.onCreateMenu(menu, menuInflater)
}
override fun onPrepareMenu(menu: Menu) {
// Check if this timeline is in a tab; if not, enable the add_to_tab menu item
val currentTabs = accountManager.activeAccount?.tabPreferences.orEmpty()
val hideMenu = currentTabs.contains(timeline)
menu.findItem(R.id.action_add_to_tab)?.setVisible(!hideMenu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_add_to_tab -> {
addToTab()
Toast.makeText(this, getString(R.string.action_add_to_tab_success, supportActionBar?.title), Toast.LENGTH_LONG).show()
menuItem.setVisible(false)
true
}
R.id.action_follow_hashtag -> {
followTag()
true
}
R.id.action_unfollow_hashtag -> {
unfollowTag()
true
}
R.id.action_mute_hashtag -> {
muteTag()
true
}
R.id.action_unmute_hashtag -> {
unmuteTag()
true
}
else -> super.onMenuItemSelected(menuItem)
}
}
private fun addToTab() {
accountManager.activeAccount?.let {
lifecycleScope.launch(Dispatchers.IO) {
it.tabPreferences += timeline
accountManager.saveAccount(it)
eventHub.dispatch(MainTabsChangedEvent(it.tabPreferences))
}
}
}
private fun followTag(): Boolean {

View File

@ -16,6 +16,7 @@
package app.pachli.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.ViewGroup
@ -26,12 +27,10 @@ import app.pachli.R
import app.pachli.TabViewData
import app.pachli.core.common.extensions.hide
import app.pachli.core.common.extensions.show
import app.pachli.core.designsystem.R as DR
import app.pachli.core.model.Timeline
import app.pachli.core.ui.BindingHolder
import app.pachli.databinding.ItemTabPreferenceBinding
import app.pachli.databinding.ItemTabPreferenceSmallBinding
import app.pachli.util.setDrawableTint
import com.google.android.material.chip.Chip
interface ItemInteractionListener {
@ -47,7 +46,6 @@ class TabAdapter(
private var data: List<TabViewData>,
private val small: Boolean,
private val listener: ItemInteractionListener,
private var removeButtonEnabled: Boolean = false,
) : RecyclerView.Adapter<BindingHolder<ViewBinding>>() {
fun updateData(newData: List<TabViewData>) {
@ -64,30 +62,26 @@ class TabAdapter(
return BindingHolder(binding)
}
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: BindingHolder<ViewBinding>, position: Int) {
val context = holder.itemView.context
val tab = data[position]
val tabViewData = data[position]
if (small) {
val binding = holder.binding as ItemTabPreferenceSmallBinding
binding.textView.setText(tab.text)
binding.textView.setText(tabViewData.text)
binding.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(tab.icon, 0, 0, 0)
binding.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(tabViewData.icon, 0, 0, 0)
binding.textView.setOnClickListener {
listener.onTabAdded(tab)
listener.onTabAdded(tabViewData)
}
} else {
val binding = holder.binding as ItemTabPreferenceBinding
if (tab.timeline is Timeline.UserList) {
binding.textView.text = tab.timeline.title
} else {
binding.textView.setText(tab.text)
}
binding.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(tab.icon, 0, 0, 0)
binding.textView.text = tabViewData.title(context)
binding.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(tabViewData.icon, 0, 0, 0)
binding.imageView.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
@ -97,17 +91,27 @@ class TabAdapter(
false
}
}
if (tabViewData.timeline !is Timeline.Home) {
binding.removeButton.setOnClickListener {
listener.onTabRemoved(holder.bindingAdapterPosition)
}
binding.removeButton.isEnabled = removeButtonEnabled
setDrawableTint(
holder.itemView.context,
binding.removeButton.drawable,
(if (removeButtonEnabled) android.R.attr.textColorTertiary else DR.attr.textColorDisabled),
)
if (tab.timeline is Timeline.Hashtags) {
binding.removeButton.show()
binding.textView.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
listener.onStartDelete(holder)
true
} else {
false
}
}
} else {
binding.removeButton.hide()
}
if (tabViewData.timeline is Timeline.Hashtags) {
// Hashtags are shown as chips, set the text back to generic "Hashtags"
binding.textView.setText(tabViewData.text)
binding.chipGroup.show()
/*
@ -115,7 +119,7 @@ class TabAdapter(
* The other dynamic chips are inserted in front of the actionChip.
* This code tries to reuse already added chips to reduce the number of Views created.
*/
tab.timeline.tags.forEachIndexed { i, arg ->
tabViewData.timeline.tags.forEachIndexed { i, arg ->
val chip = binding.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip?
?: Chip(context).apply {
@ -126,23 +130,23 @@ class TabAdapter(
chip.text = arg
if (tab.timeline.tags.size <= 1) {
if (tabViewData.timeline.tags.size <= 1) {
chip.isCloseIconVisible = false
chip.setOnClickListener(null)
} else {
chip.isCloseIconVisible = true
chip.setOnClickListener {
listener.onChipClicked(tab.timeline, holder.bindingAdapterPosition, i)
listener.onChipClicked(tabViewData.timeline, holder.bindingAdapterPosition, i)
}
}
}
while (binding.chipGroup.size - 1 > tab.timeline.tags.size) {
binding.chipGroup.removeViewAt(tab.timeline.tags.size)
while (binding.chipGroup.size - 1 > tabViewData.timeline.tags.size) {
binding.chipGroup.removeViewAt(tabViewData.timeline.tags.size)
}
binding.actionChip.setOnClickListener {
listener.onActionChipClicked(tab.timeline, holder.bindingAdapterPosition)
listener.onActionChipClicked(tabViewData.timeline, holder.bindingAdapterPosition)
}
} else {
binding.chipGroup.hide()
@ -151,11 +155,4 @@ class TabAdapter(
}
override fun getItemCount() = data.size
fun setRemoveButtonVisible(enabled: Boolean) {
if (removeButtonEnabled != enabled) {
removeButtonEnabled = enabled
notifyDataSetChanged()
}
}
}

View File

@ -1,78 +0,0 @@
/*
* Copyright 2024 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/
package app.pachli.components.notifications
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import androidx.fragment.app.commit
import app.pachli.R
import app.pachli.core.activity.BottomSheetActivity
import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.common.util.unsafeLazy
import app.pachli.core.navigation.ComposeActivityIntent
import app.pachli.databinding.ActivityNotificationsBinding
import app.pachli.interfaces.ActionButtonActivity
import app.pachli.interfaces.AppBarLayoutHost
import com.google.android.material.appbar.AppBarLayout
class NotificationsActivity : BottomSheetActivity(), ActionButtonActivity, AppBarLayoutHost, MenuProvider {
private val binding by viewBinding(ActivityNotificationsBinding::inflate)
override val actionButton by unsafeLazy { binding.composeButton }
override val appBarLayout: AppBarLayout
get() = binding.includedToolbar.appbar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
setSupportActionBar(binding.includedToolbar.toolbar)
supportActionBar?.run {
setTitle(R.string.title_notifications)
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
}
if (supportFragmentManager.findFragmentById(R.id.fragmentContainer) == null) {
supportFragmentManager.commit {
val fragment = NotificationsFragment.newInstance()
replace(R.id.fragmentContainer, fragment)
}
}
binding.composeButton.setOnClickListener {
val composeIntent = ComposeActivityIntent(applicationContext)
startActivity(composeIntent)
}
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
super.onCreateMenu(menu, menuInflater)
menuInflater.inflate(R.menu.activity_notifications, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
super.onMenuItemSelected(menuItem)
return super.onOptionsItemSelected(menuItem)
}
}

View File

@ -66,15 +66,14 @@ import app.pachli.interfaces.StatusActionListener
import app.pachli.util.ListStatusAccessibilityDelegate
import app.pachli.util.UserRefreshState
import app.pachli.util.asRefreshState
import app.pachli.util.makeIcon
import app.pachli.viewdata.NotificationViewData
import at.connyduck.sparkbutton.helpers.Utils
import com.google.android.material.color.MaterialColors
import com.google.android.material.divider.MaterialDividerItemDecoration
import com.google.android.material.snackbar.Snackbar
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.IconicsSize
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
@ -448,18 +447,11 @@ class NotificationsFragment :
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.fragment_notifications, menu)
val iconColor = MaterialColors.getColor(binding.root, android.R.attr.textColorPrimary)
menu.findItem(R.id.action_refresh)?.apply {
icon = IconicsDrawable(requireContext(), GoogleMaterial.Icon.gmd_refresh).apply {
sizeDp = 20
colorInt = iconColor
}
icon = makeIcon(requireContext(), GoogleMaterial.Icon.gmd_refresh, IconicsSize.dp(20))
}
menu.findItem(R.id.action_edit_notification_filter)?.apply {
icon = IconicsDrawable(requireContext(), GoogleMaterial.Icon.gmd_tune).apply {
sizeDp = 20
colorInt = iconColor
}
icon = makeIcon(requireContext(), GoogleMaterial.Icon.gmd_tune, IconicsSize.dp(20))
}
}

View File

@ -101,7 +101,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
preference {
setTitle(R.string.title_tab_preferences)
setIcon(R.drawable.ic_tabs)
setIcon(R.drawable.ic_add_to_tab_24)
setOnPreferenceClickListener {
val intent = TabPreferenceActivityIntent(context)
activity?.startActivity(intent)
@ -191,9 +191,11 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
val server = serverRepository.flow.value.getOrElse { null }
isEnabled = server?.let {
it.can(
ORG_JOINMASTODON_FILTERS_CLIENT, ">1.0.0".toConstraint(),
ORG_JOINMASTODON_FILTERS_CLIENT,
">1.0.0".toConstraint(),
) || it.can(
ORG_JOINMASTODON_FILTERS_SERVER, ">1.0.0".toConstraint(),
ORG_JOINMASTODON_FILTERS_SERVER,
">1.0.0".toConstraint(),
)
} ?: false
if (!isEnabled) summary = context.getString(R.string.pref_summary_timeline_filters)

View File

@ -21,33 +21,44 @@ import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.lifecycle.lifecycleScope
import app.pachli.R
import app.pachli.components.timeline.TimelineFragment
import app.pachli.TabViewData
import app.pachli.appstore.EventHub
import app.pachli.appstore.MainTabsChangedEvent
import app.pachli.core.activity.BottomSheetActivity
import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.model.Timeline
import app.pachli.core.ui.extensions.reduceSwipeSensitivity
import app.pachli.databinding.ActivityTrendingBinding
import app.pachli.interfaces.AppBarLayoutHost
import app.pachli.pager.MainPagerAdapter
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@AndroidEntryPoint
class TrendingActivity : BottomSheetActivity(), AppBarLayoutHost, MenuProvider {
@Inject
lateinit var eventHub: EventHub
private val binding: ActivityTrendingBinding by viewBinding(ActivityTrendingBinding::inflate)
override val appBarLayout: AppBarLayout
get() = binding.appBar
private lateinit var adapter: MainPagerAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
addMenuProvider(this)
setSupportActionBar(binding.toolbar)
@ -57,12 +68,26 @@ class TrendingActivity : BottomSheetActivity(), AppBarLayoutHost, MenuProvider {
setDisplayShowHomeEnabled(true)
}
val adapter = TrendingFragmentAdapter(this)
adapter = MainPagerAdapter(
listOf(
TabViewData.from(Timeline.TrendingHashtags),
TabViewData.from(Timeline.TrendingLinks),
TabViewData.from(Timeline.TrendingStatuses),
),
this,
)
binding.pager.adapter = adapter
binding.pager.reduceSwipeSensitivity()
TabLayoutMediator(binding.tabLayout, binding.pager) { tab, position ->
tab.text = adapter.title(position)
// Use a shorter tab label, as "Trending" is already in the toolbar
tab.text = when (position) {
0 -> getString(R.string.title_tab_public_trending_hashtags)
1 -> getString(R.string.title_tab_public_trending_links)
2 -> getString(R.string.title_tab_public_trending_statuses)
else -> throw IllegalStateException()
}
}.attach()
onBackPressedDispatcher.addCallback(
@ -80,30 +105,29 @@ class TrendingActivity : BottomSheetActivity(), AppBarLayoutHost, MenuProvider {
menuInflater.inflate(R.menu.activity_trending, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
super.onMenuItemSelected(menuItem)
return super.onOptionsItemSelected(menuItem)
}
}
class TrendingFragmentAdapter(val activity: FragmentActivity) : FragmentStateAdapter(activity) {
override fun getItemCount() = 3
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> TrendingTagsFragment.newInstance()
1 -> TrendingLinksFragment.newInstance()
2 -> TimelineFragment.newInstance(Timeline.TrendingStatuses)
else -> throw IllegalStateException()
}
}
fun title(position: Int): CharSequence {
return when (position) {
0 -> activity.getString(R.string.title_tab_public_trending_hashtags)
1 -> activity.getString(R.string.title_tab_public_trending_links)
2 -> activity.getString(R.string.title_tab_public_trending_statuses)
else -> throw IllegalStateException()
}
override fun onPrepareMenu(menu: Menu) {
val timeline = adapter.tabs[binding.pager.currentItem].timeline
// Check if this timeline is in a tab; if not, enable the add_to_tab menu item
val currentTabs = accountManager.activeAccount?.tabPreferences.orEmpty()
val hideMenu = currentTabs.contains(timeline)
menu.findItem(R.id.action_add_to_tab)?.setVisible(!hideMenu)
}
override fun onMenuItemSelected(menuItem: MenuItem) = when (menuItem.itemId) {
R.id.action_add_to_tab -> {
val tabViewData = adapter.tabs[binding.pager.currentItem]
val timeline = tabViewData.timeline
accountManager.activeAccount?.let {
lifecycleScope.launch(Dispatchers.IO) {
it.tabPreferences += timeline
accountManager.saveAccount(it)
eventHub.dispatch(MainTabsChangedEvent(it.tabPreferences))
}
}
Toast.makeText(this, getString(R.string.action_add_to_tab_success, tabViewData.title(this)), Toast.LENGTH_LONG).show()
menuItem.setVisible(false)
true
}
else -> super.onMenuItemSelected(menuItem)
}
}

View File

@ -0,0 +1,22 @@
<!--
~ Copyright 2024 Pachli Association
~
~ This file is a part of Pachli.
~
~ This program is free software; you can redistribute it and/or modify it under the terms of the
~ GNU General Public License as published by the Free Software Foundation; either version 3 of the
~ License, or (at your option) any later version.
~
~ Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
~ Public License for more details.
~
~ You should have received a copy of the GNU General Public License along with Pachli; if not,
~ see <http://www.gnu.org/licenses>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M21,3L3,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,19L3,19L3,5h10v4h8v10z"/>
</vector>

View File

@ -2,7 +2,8 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="?android:attr/textColorPrimary"
android:pathData="M11,18c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2 0.9,-2 2,-2 2,0.9 2,2zM9,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM9,4c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM15,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM15,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM15,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M22,3l-1.67,1.67L18.67,3L17,4.67L15.33,3l-1.66,1.67L12,3l-1.67,1.67L8.67,3L7,4.67L5.33,3L3.67,4.67L2,3v16c0,1.1 0.9,2 2,2l16,0c1.1,0 2,-0.9 2,-2V3zM11,19H4v-6h7V19zM20,19h-7v-2h7V19zM20,15h-7v-2h7V15zM20,11H4V8h16V11z"/>
</vector>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path android:fillColor="#000" android:pathData="M22,14A2,2 0 0,1 20,16H4A2,2 0 0,1 2,14V10A2,2 0 0,1 4,8H20A2,2 0 0,1 22,10V14M4,14H8V10H4V14M10,14H14V10H10V14M16,14H20V10H16V14Z" />
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M13.5,0.67s0.74,2.65 0.74,4.8c0,2.06 -1.35,3.73 -3.41,3.73 -2.07,0 -3.63,-1.67 -3.63,-3.73l0.03,-0.36C5.21,7.51 4,10.62 4,14c0,4.42 3.58,8 8,8s8,-3.58 8,-8C20,8.61 17.41,3.8 13.5,0.67zM11.71,19c-1.78,0 -3.22,-1.4 -3.22,-3.14 0,-1.62 1.05,-2.76 2.81,-3.12 1.77,-0.36 3.6,-1.21 4.62,-2.58 0.39,1.29 0.59,2.65 0.59,4.04 0,2.65 -2.15,4.8 -4.8,4.8z"/>
</vector>

View File

@ -1,48 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2024 Pachli Association
~
~ This file is a part of Pachli.
~
~ This program is free software; you can redistribute it and/or modify it under the terms of the
~ GNU General Public License as published by the Free Software Foundation; either version 3 of the
~ License, or (at your option) any later version.
~
~ Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
~ Public License for more details.
~
~ You should have received a copy of the GNU General Public License along with Pachli; if not,
~ see <http://www.gnu.org/licenses>.
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="app.pachli.components.notifications.NotificationsActivity">
<include
android:id="@+id/includedToolbar"
layout="@layout/toolbar_basic" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/composeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/fabMargin"
android:contentDescription="@string/action_compose"
app:layout_anchor="@id/fragmentContainer"
app:layout_anchorGravity="bottom|end"
app:srcCompat="@drawable/ic_create_24dp" />
<include layout="@layout/item_status_bottom_sheet" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -19,6 +19,7 @@
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:src="@drawable/ic_drag_indicator_24dp"
app:tint="?attr/colorControlNormal"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="@id/textView"/>
@ -33,7 +34,7 @@
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:textAppearance="?android:attr/textAppearanceListItemSmall"
app:drawableTint="?android:attr/textColorSecondary"
app:drawableTint="?android:attr/colorControlNormal"
app:layout_constraintBottom_toTopOf="@id/chipGroup"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView"
@ -53,7 +54,8 @@
android:contentDescription="@string/action_delete"
android:src="@drawable/ic_clear_24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/colorControlNormal" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/chipGroup"

View File

@ -22,4 +22,14 @@
android:id="@+id/action_search"
android:title="@string/action_search"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_remove_tab"
android:title="@string/action_remove_tab"
app:showAsAction="never" />
<item
android:id="@+id/action_tab_preferences"
android:title="@string/action_manage_tabs"
app:showAsAction="never" />
</menu>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2024 Pachli Association
~ Copyright 2023 Pachli Association
~
~ This file is a part of Pachli.
~
@ -18,4 +18,9 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_add_to_tab"
android:title="@string/action_add_to_tab"
android:icon="@drawable/ic_add_to_tab_24"
app:showAsAction="ifRoom"/>
</menu>

View File

@ -18,4 +18,9 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_add_to_tab"
android:title="@string/action_add_to_tab"
android:icon="@drawable/ic_add_to_tab_24"
app:showAsAction="ifRoom"/>
</menu>

View File

@ -7,28 +7,28 @@
android:id="@+id/action_follow_hashtag"
android:title="@string/action_follow"
app:showAsAction="ifRoom"
app:iconTint="?attr/colorOnSurface"
app:iconTint="?attr/colorControlNormal"
android:icon="@drawable/ic_person_add_24dp" />
<item
android:id="@+id/action_unfollow_hashtag"
android:title="@string/action_unfollow"
app:showAsAction="ifRoom"
app:iconTint="?attr/colorOnSurface"
app:iconTint="?attr/colorControlNormal"
android:icon="@drawable/ic_person_remove_24dp" />
<item
android:id="@+id/action_mute_hashtag"
android:title="@string/action_mute"
app:showAsAction="ifRoom"
app:iconTint="?attr/colorOnSurface"
app:iconTint="?attr/colorControlNormal"
android:icon="@drawable/ic_mute_24dp" />
<item
android:id="@+id/action_unmute_hashtag"
android:title="@string/action_unmute"
app:showAsAction="ifRoom"
app:iconTint="?attr/colorOnSurface"
app:iconTint="?attr/colorControlNormal"
android:icon="@drawable/ic_unmute_24dp" />
</menu>

View File

@ -701,5 +701,9 @@
<string name="pref_update_check_no_updates">There are no updates available</string>
<string name="pref_update_next_scheduled_check">Next scheduled check: %1$s</string>
<string name="error_media_download">Could not download %1$s: %2$d %3$s</string>
<string name="action_add_to_tab">Add to tab</string>
<string name="action_add_to_tab_success">Added \'%1$s\' to tabs</string>
<string name="action_remove_tab">Remove tab</string>
<string name="action_manage_tabs">Manage tabs</string>
</resources>

View File

@ -466,6 +466,15 @@ class TimelineActivityIntent private constructor(context: Context) : Intent() {
putExtra(EXTRA_TIMELINE, Timeline.PublicLocal)
}
/**
* Show notifications timeline
*
* @param context
*/
fun notifications(context: Context) = TimelineActivityIntent(context).apply {
putExtra(EXTRA_TIMELINE, Timeline.Notifications)
}
/** @return The [Timeline] to show */
fun getTimeline(intent: Intent) = IntentCompat.getParcelableExtra(intent, EXTRA_TIMELINE, Timeline::class.java)!!
}
@ -593,12 +602,6 @@ class LoginWebViewActivityIntent(context: Context) : Intent() {
}
}
class NotificationsActivityIntent(context: Context) : Intent() {
init {
setClassName(context, QuadrantConstants.NOTIFICATIONS_ACTIVITY)
}
}
class ScheduledStatusActivityIntent(context: Context) : Intent() {
init {
setClassName(context, QuadrantConstants.SCHEDULED_STATUS_ACTIVITY)