From 0c48dcf06cd69f5f7db43b730d8d06e93d7d891f Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 24 Mar 2019 08:59:55 +0100 Subject: [PATCH] add hashtag tabs (#1145) * add hashtag tabs * address review feedback --- .../java/com/keylesspalace/tusky/TabData.kt | 18 ++-- .../tusky/TabPreferenceActivity.kt | 85 ++++++++++++++++--- .../keylesspalace/tusky/adapter/TabAdapter.kt | 41 ++++++--- .../com/keylesspalace/tusky/db/Converters.kt | 7 +- .../tusky/pager/MainPagerAdapter.kt | 5 +- .../tusky/util/ViewExtensions.kt | 10 +-- .../main/res/drawable-v26/ic_edit_chip.xml | 11 +++ app/src/main/res/drawable/ic_edit_chip.xml | 9 ++ app/src/main/res/drawable/ic_hashtag.xml | 7 ++ .../main/res/layout/item_tab_preference.xml | 69 ++++++++++----- app/src/main/res/values/strings.xml | 5 ++ 11 files changed, 209 insertions(+), 58 deletions(-) create mode 100644 app/src/main/res/drawable-v26/ic_edit_chip.xml create mode 100644 app/src/main/res/drawable/ic_edit_chip.xml create mode 100644 app/src/main/res/drawable/ic_hashtag.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/TabData.kt b/app/src/main/java/com/keylesspalace/tusky/TabData.kt index 20cd76c07..5e6b0d39d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TabData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TabData.kt @@ -29,20 +29,22 @@ const val NOTIFICATIONS = "Notifications" const val LOCAL = "Local" const val FEDERATED = "Federated" const val DIRECT = "Direct" +const val HASHTAG = "Hashtag" data class TabData(val id: String, @StringRes val text: Int, @DrawableRes val icon: Int, - val fragment: () -> Fragment) + val fragment: (List) -> Fragment, + val arguments: List = emptyList()) - -fun createTabDataFromId(id: String): TabData { +fun createTabDataFromId(id: String, arguments: List = emptyList()): TabData { return when (id) { - HOME -> TabData(HOME, R.string.title_home, R.drawable.ic_home_24dp) { TimelineFragment.newInstance(TimelineFragment.Kind.HOME) } - NOTIFICATIONS -> TabData(NOTIFICATIONS, R.string.title_notifications, R.drawable.ic_notifications_24dp) { NotificationsFragment.newInstance() } - LOCAL -> TabData(LOCAL, R.string.title_public_local, R.drawable.ic_local_24dp) { TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_LOCAL) } - FEDERATED -> TabData(FEDERATED, R.string.title_public_federated, R.drawable.ic_public_24dp) { TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_FEDERATED) } - DIRECT -> TabData(DIRECT, R.string.title_direct_messages, R.drawable.reblog_direct_dark) { ConversationsFragment.newInstance() } + HOME -> TabData(HOME, R.string.title_home, R.drawable.ic_home_24dp, { TimelineFragment.newInstance(TimelineFragment.Kind.HOME) }) + NOTIFICATIONS -> TabData(NOTIFICATIONS, R.string.title_notifications, R.drawable.ic_notifications_24dp, { NotificationsFragment.newInstance() }) + LOCAL -> TabData(LOCAL, R.string.title_public_local, R.drawable.ic_local_24dp, { TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_LOCAL) }) + FEDERATED -> TabData(FEDERATED, R.string.title_public_federated, R.drawable.ic_public_24dp, { TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_FEDERATED) }) + DIRECT -> TabData(DIRECT, R.string.title_direct_messages, R.drawable.reblog_direct_dark, { ConversationsFragment.newInstance() }) + HASHTAG -> TabData(HASHTAG, R.string.hashtag, R.drawable.ic_hashtag, { args -> TimelineFragment.newInstance(TimelineFragment.Kind.TAG, args.getOrNull(0).orEmpty()) }, arguments) else -> throw IllegalArgumentException("unknown tab type") } } diff --git a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt index 10429c6d8..918d361da 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt @@ -17,6 +17,8 @@ package com.keylesspalace.tusky import android.os.Bundle import android.view.MenuItem +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.AppCompatEditText import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.ItemTouchHelper @@ -27,16 +29,17 @@ import com.keylesspalace.tusky.adapter.TabAdapter import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.MainTabsChangedEvent import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.util.onTextChanged import com.keylesspalace.tusky.util.visible +import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from +import com.uber.autodispose.autoDisposable import io.reactivex.Single import io.reactivex.schedulers.Schedulers import kotlinx.android.synthetic.main.activity_tab_preference.* import kotlinx.android.synthetic.main.toolbar_basic.* +import java.util.regex.Pattern import javax.inject.Inject -import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from -import com.uber.autodispose.autoDisposable - class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListener { @Inject @@ -51,6 +54,8 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene private val selectedItemElevation by lazy { resources.getDimension(R.dimen.selected_drag_item_elevation) } + private val hashtagRegex by lazy { Pattern.compile("([\\w_]*[\\p{Alpha}_][\\w_]*)", Pattern.CASE_INSENSITIVE) } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -74,7 +79,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene addTabRecyclerView.adapter = addTabAdapter addTabRecyclerView.layoutManager = LinearLayoutManager(this) - touchHelper = ItemTouchHelper(object: ItemTouchHelper.Callback(){ + touchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() { override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.END) } @@ -105,7 +110,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene } override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { - if(actionState == ItemTouchHelper.ACTION_STATE_DRAG) { + if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { viewHolder?.itemView?.elevation = selectedItemElevation } } @@ -134,37 +139,93 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene } override fun onTabAdded(tab: TabData) { + + if (currentTabs.size >= MAX_TAB_COUNT) { + return + } + + actionButton.isExpanded = false + + if (tab.id == HASHTAG) { + showEditHashtagDialog() + return + } + currentTabs.add(tab) currentTabsAdapter.notifyItemInserted(currentTabs.size - 1) - actionButton.isExpanded = false updateAvailableTabs() saveTabs() } + override fun onActionChipClicked(tab: TabData) { + showEditHashtagDialog(tab) + } + + private fun showEditHashtagDialog(tab: TabData? = null) { + + val editText = AppCompatEditText(this) + editText.setHint(R.string.edit_hashtag_hint) + editText.setText("") + editText.append(tab?.arguments?.first().orEmpty()) + + val dialog = AlertDialog.Builder(this) + .setTitle(R.string.edit_hashtag_title) + .setView(editText) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.action_save) { _, _ -> + val input = editText.text.toString().trim() + if (tab == null) { + val newTab = createTabDataFromId(HASHTAG, listOf(input)) + currentTabs.add(newTab) + currentTabsAdapter.notifyItemInserted(currentTabs.size - 1) + } else { + val newTab = tab.copy(arguments = listOf(input)) + val position = currentTabs.indexOf(tab) + currentTabs[position] = newTab + + currentTabsAdapter.notifyItemChanged(position) + } + + updateAvailableTabs() + saveTabs() + } + .create() + + editText.onTextChanged { s, _, _, _ -> + val input = s.trim() + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = input.isNotEmpty() && hashtagRegex.matcher(input).matches() + } + + dialog.show() + editText.requestFocus() + } + private fun updateAvailableTabs() { val addableTabs: MutableList = mutableListOf() val homeTab = createTabDataFromId(HOME) - if(!currentTabs.contains(homeTab)) { + if (!currentTabs.contains(homeTab)) { addableTabs.add(homeTab) } val notificationTab = createTabDataFromId(NOTIFICATIONS) - if(!currentTabs.contains(notificationTab)) { + if (!currentTabs.contains(notificationTab)) { addableTabs.add(notificationTab) } val localTab = createTabDataFromId(LOCAL) - if(!currentTabs.contains(localTab)) { + if (!currentTabs.contains(localTab)) { addableTabs.add(localTab) } val federatedTab = createTabDataFromId(FEDERATED) - if(!currentTabs.contains(federatedTab)) { + if (!currentTabs.contains(federatedTab)) { addableTabs.add(federatedTab) } val directMessagesTab = createTabDataFromId(DIRECT) - if(!currentTabs.contains(directMessagesTab)) { + if (!currentTabs.contains(directMessagesTab)) { addableTabs.add(directMessagesTab) } + addableTabs.add(createTabDataFromId(HASHTAG)) + addTabAdapter.updateData(addableTabs) maxTabsInfo.visible(addableTabs.size == 0 || currentTabs.size >= MAX_TAB_COUNT) @@ -211,7 +272,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene override fun onPause() { super.onPause() - if(tabsChanged) { + if (tabsChanged) { eventHub.dispatch(MainTabsChangedEvent(currentTabs)) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt index 286718271..dc08af5bf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt @@ -19,22 +19,26 @@ import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.ViewGroup - import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.HASHTAG import com.keylesspalace.tusky.R import com.keylesspalace.tusky.TabData import com.keylesspalace.tusky.util.ThemeUtils +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show import kotlinx.android.synthetic.main.item_tab_preference.view.* + interface ItemInteractionListener { fun onTabAdded(tab: TabData) fun onStartDelete(viewHolder: RecyclerView.ViewHolder) fun onStartDrag(viewHolder: RecyclerView.ViewHolder) + fun onActionChipClicked(tab: TabData) } -class TabAdapter(var data: List, - val small: Boolean = false, - val listener: ItemInteractionListener? = null) : RecyclerView.Adapter() { +class TabAdapter(private var data: List, + private val small: Boolean = false, + private val listener: ItemInteractionListener? = null) : RecyclerView.Adapter() { fun updateData(newData: List) { this.data = newData @@ -42,7 +46,7 @@ class TabAdapter(var data: List, } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val layoutId = if(small) { + val layoutId = if (small) { R.layout.item_tab_preference_small } else { R.layout.item_tab_preference @@ -52,26 +56,43 @@ class TabAdapter(var data: List, } override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val context = holder.itemView.context holder.itemView.textView.setText(data[position].text) - val iconDrawable = ThemeUtils.getTintedDrawable(holder.itemView.context, data[position].icon, android.R.attr.textColorSecondary) + val iconDrawable = ThemeUtils.getTintedDrawable(context, data[position].icon, android.R.attr.textColorSecondary) holder.itemView.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(iconDrawable, null, null, null) - if(small) { + if (small) { holder.itemView.textView.setOnClickListener { listener?.onTabAdded(data[position]) } } holder.itemView.imageView?.setOnTouchListener { _, event -> - if(event.action == MotionEvent.ACTION_DOWN) { + if (event.action == MotionEvent.ACTION_DOWN) { listener?.onStartDrag(holder) true } else { false } } + + if (!small) { + + if (data[position].id == HASHTAG) { + holder.itemView.chipGroup.show() + holder.itemView.actionChip.text = data[position].arguments[0] + + holder.itemView.actionChip.setChipIconResource(R.drawable.ic_edit_chip) + + holder.itemView.actionChip.chipIcon = context.getDrawable(R.drawable.ic_edit_chip) + holder.itemView.actionChip.setOnClickListener { + listener?.onActionChipClicked(data[position]) + } + + } else { + holder.itemView.chipGroup.hide() + } + } } - - override fun getItemCount(): Int { return data.size } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt index 20181010b..fec036ba4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt @@ -58,12 +58,15 @@ class Converters { @TypeConverter fun stringToTabData(str: String?): List? { return str?.split(";") - ?.map { createTabDataFromId(it) } + ?.map { + val data = it.split(":") + createTabDataFromId(data[0], data.drop(1)) + } } @TypeConverter fun tabDataToString(tabData: List?): String? { - return tabData?.joinToString(";") { it.id } + return tabData?.joinToString(";") { it.id + ":" + it.arguments.joinToString(":") } } @TypeConverter diff --git a/app/src/main/java/com/keylesspalace/tusky/pager/MainPagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/pager/MainPagerAdapter.kt index fef20bf25..668faf549 100644 --- a/app/src/main/java/com/keylesspalace/tusky/pager/MainPagerAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/pager/MainPagerAdapter.kt @@ -24,7 +24,8 @@ import com.keylesspalace.tusky.TabData class MainPagerAdapter(val tabs: List, manager: FragmentManager) : FragmentPagerAdapter(manager) { override fun getItem(position: Int): Fragment { - return tabs[position].fragment() + val tab = tabs[position] + return tab.fragment(tab.arguments) } override fun getCount(): Int { @@ -36,7 +37,7 @@ class MainPagerAdapter(val tabs: List, manager: FragmentManager) : Frag } override fun getItemId(position: Int): Long { - return tabs[position].id.hashCode().toLong() + return tabs[position].hashCode() + position.toLong() } override fun getItemPosition(item: Any): Int { diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt index 2e4210153..b976a5d63 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt @@ -34,20 +34,20 @@ fun View.visible(visible: Boolean, or: Int = View.GONE) { } open class DefaultTextWatcher : TextWatcher { - override fun afterTextChanged(s: Editable?) { + override fun afterTextChanged(s: Editable) { } - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { } - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { } } inline fun EditText.onTextChanged( - crossinline callback: (s: CharSequence?, start: Int, before: Int, count: Int) -> Unit) { + crossinline callback: (s: CharSequence, start: Int, before: Int, count: Int) -> Unit) { addTextChangedListener(object : DefaultTextWatcher() { - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { callback(s, start, before, count) } }) diff --git a/app/src/main/res/drawable-v26/ic_edit_chip.xml b/app/src/main/res/drawable-v26/ic_edit_chip.xml new file mode 100644 index 000000000..a843e4c22 --- /dev/null +++ b/app/src/main/res/drawable-v26/ic_edit_chip.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_edit_chip.xml b/app/src/main/res/drawable/ic_edit_chip.xml new file mode 100644 index 000000000..99a54009e --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_chip.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_hashtag.xml b/app/src/main/res/drawable/ic_hashtag.xml new file mode 100644 index 000000000..c7a3bc010 --- /dev/null +++ b/app/src/main/res/drawable/ic_hashtag.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_tab_preference.xml b/app/src/main/res/layout/item_tab_preference.xml index abb866ff2..e76c23999 100644 --- a/app/src/main/res/layout/item_tab_preference.xml +++ b/app/src/main/res/layout/item_tab_preference.xml @@ -1,28 +1,59 @@ - + + + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - + + - + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 19731fbd6..ade2bd7f7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -457,4 +457,9 @@ List name + Edit hashtag + Hashtag without # + Hashtag + +