From 6bef6f2fae13a99012d15b9d40fa033b918c0ac4 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Wed, 3 Apr 2024 00:10:09 +0200 Subject: [PATCH] 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 --- app/lint-baseline.xml | 37 +------- app/src/main/AndroidManifest.xml | 1 - app/src/main/java/app/pachli/MainActivity.kt | 41 ++++++--- .../java/app/pachli/TabPreferenceActivity.kt | 15 +--- app/src/main/java/app/pachli/TabViewData.kt | 5 +- .../main/java/app/pachli/TimelineActivity.kt | 71 ++++++++++++--- .../java/app/pachli/adapter/TabAdapter.kt | 71 ++++++++------- .../notifications/NotificationsActivity.kt | 78 ----------------- .../notifications/NotificationsFragment.kt | 16 +--- .../preference/AccountPreferencesFragment.kt | 8 +- .../components/trending/TrendingActivity.kt | 86 ++++++++++++------- .../main/res/drawable/ic_add_to_tab_24.xml | 22 +++++ .../res/drawable/ic_drag_indicator_24dp.xml | 3 +- app/src/main/res/drawable/ic_newspaper_24.xml | 5 ++ app/src/main/res/drawable/ic_tabs.xml | 9 -- app/src/main/res/drawable/ic_whatshot_24.xml | 5 ++ .../res/layout/activity_notifications.xml | 48 ----------- .../main/res/layout/item_tab_preference.xml | 6 +- app/src/main/res/menu/activity_main.xml | 10 +++ ...otifications.xml => activity_timeline.xml} | 7 +- app/src/main/res/menu/activity_trending.xml | 5 ++ .../main/res/menu/view_hashtag_toolbar.xml | 8 +- app/src/main/res/values/strings.xml | 4 + .../app/pachli/core/navigation/Navigation.kt | 15 ++-- 24 files changed, 270 insertions(+), 306 deletions(-) delete mode 100644 app/src/main/java/app/pachli/components/notifications/NotificationsActivity.kt create mode 100644 app/src/main/res/drawable/ic_add_to_tab_24.xml create mode 100644 app/src/main/res/drawable/ic_newspaper_24.xml delete mode 100644 app/src/main/res/drawable/ic_tabs.xml create mode 100644 app/src/main/res/drawable/ic_whatshot_24.xml delete mode 100644 app/src/main/res/layout/activity_notifications.xml rename app/src/main/res/menu/{activity_notifications.xml => activity_timeline.xml} (79%) diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml index d1f475ce5..b03cdb26a 100644 --- a/app/lint-baseline.xml +++ b/app/lint-baseline.xml @@ -949,21 +949,10 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> - - - - @@ -2621,28 +2610,6 @@ column="5"/> - - - - - - - - - diff --git a/app/src/main/java/app/pachli/MainActivity.kt b/app/src/main/java/app/pachli/MainActivity.kt index 89643f5f3..664a2f922 100644 --- a/app/src/main/java/app/pachli/MainActivity.kt +++ b/app/src/main/java/app/pachli/MainActivity.kt @@ -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.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) diff --git a/app/src/main/java/app/pachli/TabPreferenceActivity.kt b/app/src/main/java/app/pachli/TabPreferenceActivity.kt index acabb7f88..2ba80d618 100644 --- a/app/src/main/java/app/pachli/TabPreferenceActivity.kt +++ b/app/src/main/java/app/pachli/TabPreferenceActivity.kt @@ -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 - } } diff --git a/app/src/main/java/app/pachli/TabViewData.kt b/app/src/main/java/app/pachli/TabViewData.kt index 7c9758d31..ca7cd6fb0 100644 --- a/app/src/main/java/app/pachli/TabViewData.kt +++ b/app/src/main/java/app/pachli/TabViewData.kt @@ -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( diff --git a/app/src/main/java/app/pachli/TimelineActivity.kt b/app/src/main/java/app/pachli/TimelineActivity.kt index 61bcc8b8e..f92d5b120 100644 --- a/app/src/main/java/app/pachli/TimelineActivity.kt +++ b/app/src/main/java/app/pachli/TimelineActivity.kt @@ -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 { diff --git a/app/src/main/java/app/pachli/adapter/TabAdapter.kt b/app/src/main/java/app/pachli/adapter/TabAdapter.kt index 545799114..05c61f6bd 100644 --- a/app/src/main/java/app/pachli/adapter/TabAdapter.kt +++ b/app/src/main/java/app/pachli/adapter/TabAdapter.kt @@ -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, private val small: Boolean, private val listener: ItemInteractionListener, - private var removeButtonEnabled: Boolean = false, ) : RecyclerView.Adapter>() { fun updateData(newData: List) { @@ -64,30 +62,26 @@ class TabAdapter( return BindingHolder(binding) } + @SuppressLint("ClickableViewAccessibility") override fun onBindViewHolder(holder: BindingHolder, 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 } } - 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 (tabViewData.timeline !is Timeline.Home) { + binding.removeButton.setOnClickListener { + listener.onTabRemoved(holder.bindingAdapterPosition) + } - 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() - } - } } diff --git a/app/src/main/java/app/pachli/components/notifications/NotificationsActivity.kt b/app/src/main/java/app/pachli/components/notifications/NotificationsActivity.kt deleted file mode 100644 index 0e93e4ab7..000000000 --- a/app/src/main/java/app/pachli/components/notifications/NotificationsActivity.kt +++ /dev/null @@ -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 . - */ - -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) - } -} diff --git a/app/src/main/java/app/pachli/components/notifications/NotificationsFragment.kt b/app/src/main/java/app/pachli/components/notifications/NotificationsFragment.kt index 5e6d7a5c1..91e01785c 100644 --- a/app/src/main/java/app/pachli/components/notifications/NotificationsFragment.kt +++ b/app/src/main/java/app/pachli/components/notifications/NotificationsFragment.kt @@ -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)) } } diff --git a/app/src/main/java/app/pachli/components/preference/AccountPreferencesFragment.kt b/app/src/main/java/app/pachli/components/preference/AccountPreferencesFragment.kt index 1ceadc2ad..5200a668c 100644 --- a/app/src/main/java/app/pachli/components/preference/AccountPreferencesFragment.kt +++ b/app/src/main/java/app/pachli/components/preference/AccountPreferencesFragment.kt @@ -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) diff --git a/app/src/main/java/app/pachli/components/trending/TrendingActivity.kt b/app/src/main/java/app/pachli/components/trending/TrendingActivity.kt index 1bf5ac1ec..0e2cb897a 100644 --- a/app/src/main/java/app/pachli/components/trending/TrendingActivity.kt +++ b/app/src/main/java/app/pachli/components/trending/TrendingActivity.kt @@ -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) } } diff --git a/app/src/main/res/drawable/ic_add_to_tab_24.xml b/app/src/main/res/drawable/ic_add_to_tab_24.xml new file mode 100644 index 000000000..179ae2fda --- /dev/null +++ b/app/src/main/res/drawable/ic_add_to_tab_24.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_drag_indicator_24dp.xml b/app/src/main/res/drawable/ic_drag_indicator_24dp.xml index ab9d5f320..6d4355fe9 100644 --- a/app/src/main/res/drawable/ic_drag_indicator_24dp.xml +++ b/app/src/main/res/drawable/ic_drag_indicator_24dp.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24" - android:viewportHeight="24"> + android:viewportHeight="24" + android:tint="?attr/colorControlNormal"> diff --git a/app/src/main/res/drawable/ic_newspaper_24.xml b/app/src/main/res/drawable/ic_newspaper_24.xml new file mode 100644 index 000000000..a1f1836aa --- /dev/null +++ b/app/src/main/res/drawable/ic_newspaper_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_tabs.xml b/app/src/main/res/drawable/ic_tabs.xml deleted file mode 100644 index 43304cd5d..000000000 --- a/app/src/main/res/drawable/ic_tabs.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_whatshot_24.xml b/app/src/main/res/drawable/ic_whatshot_24.xml new file mode 100644 index 000000000..cf628de4e --- /dev/null +++ b/app/src/main/res/drawable/ic_whatshot_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/activity_notifications.xml b/app/src/main/res/layout/activity_notifications.xml deleted file mode 100644 index 66589c32a..000000000 --- a/app/src/main/res/layout/activity_notifications.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_tab_preference.xml b/app/src/main/res/layout/item_tab_preference.xml index 44fdaff66..dc8a7dd7a 100644 --- a/app/src/main/res/layout/item_tab_preference.xml +++ b/app/src/main/res/layout/item_tab_preference.xml @@ -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" /> + + + + diff --git a/app/src/main/res/menu/activity_notifications.xml b/app/src/main/res/menu/activity_timeline.xml similarity index 79% rename from app/src/main/res/menu/activity_notifications.xml rename to app/src/main/res/menu/activity_timeline.xml index 87805f020..6f1ea2d3a 100644 --- a/app/src/main/res/menu/activity_notifications.xml +++ b/app/src/main/res/menu/activity_timeline.xml @@ -1,6 +1,6 @@