From 27f6976295a0c83d19393fc08e52b7c78a86d33d Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Mon, 20 Feb 2023 20:14:16 +0100 Subject: [PATCH] Add FAB to follow new hashtags from FollowedTagsActivity (#3275) - Add a FAB for user interaction (hide on scroll if appropriate) - Show a dialog to collect the new hashtag - Autocomplete hashtags the same as when composing a status --- .../followedtags/FollowedTagsActivity.kt | 77 ++++++++++++++++++- .../followedtags/FollowedTagsViewModel.kt | 22 +++++- .../res/layout/activity_followed_tags.xml | 11 ++- .../main/res/layout/dialog_follow_hashtag.xml | 16 ++++ app/src/main/res/values/strings.xml | 3 + 5 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 app/src/main/res/layout/dialog_follow_hashtag.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsActivity.kt index 0b8e7a5c7..94a0b47ef 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsActivity.kt @@ -1,21 +1,30 @@ package com.keylesspalace.tusky.components.followedtags +import android.app.Dialog +import android.content.DialogInterface +import android.content.SharedPreferences import android.os.Bundle import android.util.Log +import android.widget.AutoCompleteTextView import androidx.activity.viewModels +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import androidx.paging.LoadState import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator import at.connyduck.calladapter.networkresult.fold import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.compose.ComposeAutoCompleteAdapter import com.keylesspalace.tusky.databinding.ActivityFollowedTagsBinding import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.interfaces.HashtagActionListener import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding @@ -25,13 +34,19 @@ import kotlinx.coroutines.launch import java.io.IOException import javax.inject.Inject -class FollowedTagsActivity : BaseActivity(), HashtagActionListener { +class FollowedTagsActivity : + BaseActivity(), + HashtagActionListener, + ComposeAutoCompleteAdapter.AutocompletionProvider { @Inject lateinit var api: MastodonApi @Inject lateinit var viewModelFactory: ViewModelFactory + @Inject + lateinit var sharedPreferences: SharedPreferences + private val binding by viewBinding(ActivityFollowedTagsBinding::inflate) private val viewModel: FollowedTagsViewModel by viewModels { viewModelFactory } @@ -47,6 +62,11 @@ class FollowedTagsActivity : BaseActivity(), HashtagActionListener { setDisplayShowHomeEnabled(true) } + binding.fab.setOnClickListener { + val dialog: DialogFragment = FollowTagDialog.newInstance() + dialog.show(supportFragmentManager, "dialog") + } + setupAdapter().let { adapter -> setupRecyclerView(adapter) @@ -64,6 +84,19 @@ class FollowedTagsActivity : BaseActivity(), HashtagActionListener { binding.followedTagsView.layoutManager = LinearLayoutManager(this) binding.followedTagsView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) (binding.followedTagsView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + + val hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false) + if (hideFab) { + binding.followedTagsView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + if (dy > 0 && binding.fab.isShown) { + binding.fab.hide() + } else if (dy < 0 && !binding.fab.isShown) { + binding.fab.show() + } + } + }) + } } private fun setupAdapter(): FollowedTagsAdapter { @@ -89,11 +122,15 @@ class FollowedTagsActivity : BaseActivity(), HashtagActionListener { } } - private fun follow(tagName: String, position: Int) { + private fun follow(tagName: String, position: Int = -1) { lifecycleScope.launch { api.followTag(tagName).fold( { - viewModel.tags.add(position, it) + if (position == -1) { + viewModel.tags.add(it) + } else { + viewModel.tags.add(position, it) + } viewModel.currentSource?.invalidate() }, { @@ -142,7 +179,41 @@ class FollowedTagsActivity : BaseActivity(), HashtagActionListener { } } + override fun search(token: String): List { + return viewModel.searchAutocompleteSuggestions(token) + } + companion object { const val TAG = "FollowedTagsActivity" } + + class FollowTagDialog : DialogFragment() { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val layout = layoutInflater.inflate(R.layout.dialog_follow_hashtag, null) + val autoCompleteTextView = layout.findViewById(R.id.hashtag)!! + autoCompleteTextView.setAdapter( + ComposeAutoCompleteAdapter( + requireActivity() as FollowedTagsActivity, + animateAvatar = false, + animateEmojis = false, + showBotBadge = false + ) + ) + + return AlertDialog.Builder(requireActivity()) + .setTitle(R.string.dialog_follow_hashtag_title) + .setView(layout) + .setPositiveButton(android.R.string.ok) { _, _ -> + (requireActivity() as FollowedTagsActivity).follow( + autoCompleteTextView.text.toString().removePrefix("#") + ) + } + .setNegativeButton(android.R.string.cancel) { _: DialogInterface, _: Int -> } + .create() + } + + companion object { + fun newInstance(): FollowTagDialog = FollowTagDialog() + } + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsViewModel.kt index bcb23a268..1a1b794bb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsViewModel.kt @@ -1,18 +1,22 @@ package com.keylesspalace.tusky.components.followedtags +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.ExperimentalPagingApi import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.cachedIn +import at.connyduck.calladapter.networkresult.fold +import com.keylesspalace.tusky.components.compose.ComposeAutoCompleteAdapter +import com.keylesspalace.tusky.components.search.SearchType import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.network.MastodonApi import javax.inject.Inject class FollowedTagsViewModel @Inject constructor( - api: MastodonApi + private val api: MastodonApi ) : ViewModel(), Injectable { val tags: MutableList = mutableListOf() var nextKey: String? = null @@ -28,6 +32,20 @@ class FollowedTagsViewModel @Inject constructor( ).also { source -> currentSource = source } - }, + } ).flow.cachedIn(viewModelScope) + + fun searchAutocompleteSuggestions(token: String): List { + return api.searchSync(query = token, type = SearchType.Hashtag.apiParameter, limit = 10) + .fold({ searchResult -> + searchResult.hashtags.map { ComposeAutoCompleteAdapter.AutocompleteResult.HashtagResult(it.name) } + }, { e -> + Log.e(TAG, "Autocomplete search for $token failed.", e) + emptyList() + }) + } + + companion object { + private const val TAG = "FollowedTagsViewModel" + } } diff --git a/app/src/main/res/layout/activity_followed_tags.xml b/app/src/main/res/layout/activity_followed_tags.xml index f26027571..412b43105 100644 --- a/app/src/main/res/layout/activity_followed_tags.xml +++ b/app/src/main/res/layout/activity_followed_tags.xml @@ -35,4 +35,13 @@ app:layout_behavior="@string/appbar_scrolling_view_behavior" /> - \ No newline at end of file + + + diff --git a/app/src/main/res/layout/dialog_follow_hashtag.xml b/app/src/main/res/layout/dialog_follow_hashtag.xml new file mode 100644 index 000000000..cdcf3024b --- /dev/null +++ b/app/src/main/res/layout/dialog_follow_hashtag.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b6be0ec5c..7a08a1ba8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -58,6 +58,9 @@ Followed hashtags Edits + Follow hashtag + #hashtag + \@%s %s boosted Sensitive content