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
This commit is contained in:
Nik Clayton 2023-02-20 20:14:16 +01:00 committed by GitHub
parent 41d493e72a
commit 27f6976295
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 123 additions and 6 deletions

View File

@ -1,21 +1,30 @@
package com.keylesspalace.tusky.components.followedtags package com.keylesspalace.tusky.components.followedtags
import android.app.Dialog
import android.content.DialogInterface
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.AutoCompleteTextView
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import at.connyduck.calladapter.networkresult.fold import at.connyduck.calladapter.networkresult.fold
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.compose.ComposeAutoCompleteAdapter
import com.keylesspalace.tusky.databinding.ActivityFollowedTagsBinding import com.keylesspalace.tusky.databinding.ActivityFollowedTagsBinding
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.interfaces.HashtagActionListener import com.keylesspalace.tusky.interfaces.HashtagActionListener
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
@ -25,13 +34,19 @@ import kotlinx.coroutines.launch
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
class FollowedTagsActivity : BaseActivity(), HashtagActionListener { class FollowedTagsActivity :
BaseActivity(),
HashtagActionListener,
ComposeAutoCompleteAdapter.AutocompletionProvider {
@Inject @Inject
lateinit var api: MastodonApi lateinit var api: MastodonApi
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
@Inject
lateinit var sharedPreferences: SharedPreferences
private val binding by viewBinding(ActivityFollowedTagsBinding::inflate) private val binding by viewBinding(ActivityFollowedTagsBinding::inflate)
private val viewModel: FollowedTagsViewModel by viewModels { viewModelFactory } private val viewModel: FollowedTagsViewModel by viewModels { viewModelFactory }
@ -47,6 +62,11 @@ class FollowedTagsActivity : BaseActivity(), HashtagActionListener {
setDisplayShowHomeEnabled(true) setDisplayShowHomeEnabled(true)
} }
binding.fab.setOnClickListener {
val dialog: DialogFragment = FollowTagDialog.newInstance()
dialog.show(supportFragmentManager, "dialog")
}
setupAdapter().let { adapter -> setupAdapter().let { adapter ->
setupRecyclerView(adapter) setupRecyclerView(adapter)
@ -64,6 +84,19 @@ class FollowedTagsActivity : BaseActivity(), HashtagActionListener {
binding.followedTagsView.layoutManager = LinearLayoutManager(this) binding.followedTagsView.layoutManager = LinearLayoutManager(this)
binding.followedTagsView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) binding.followedTagsView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
(binding.followedTagsView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (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 { 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 { lifecycleScope.launch {
api.followTag(tagName).fold( 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() viewModel.currentSource?.invalidate()
}, },
{ {
@ -142,7 +179,41 @@ class FollowedTagsActivity : BaseActivity(), HashtagActionListener {
} }
} }
override fun search(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
return viewModel.searchAutocompleteSuggestions(token)
}
companion object { companion object {
const val TAG = "FollowedTagsActivity" 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<AutoCompleteTextView>(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()
}
}
} }

View File

@ -1,18 +1,22 @@
package com.keylesspalace.tusky.components.followedtags package com.keylesspalace.tusky.components.followedtags
import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.ExperimentalPagingApi import androidx.paging.ExperimentalPagingApi
import androidx.paging.Pager import androidx.paging.Pager
import androidx.paging.PagingConfig import androidx.paging.PagingConfig
import androidx.paging.cachedIn 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.di.Injectable
import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import javax.inject.Inject import javax.inject.Inject
class FollowedTagsViewModel @Inject constructor( class FollowedTagsViewModel @Inject constructor(
api: MastodonApi private val api: MastodonApi
) : ViewModel(), Injectable { ) : ViewModel(), Injectable {
val tags: MutableList<HashTag> = mutableListOf() val tags: MutableList<HashTag> = mutableListOf()
var nextKey: String? = null var nextKey: String? = null
@ -28,6 +32,20 @@ class FollowedTagsViewModel @Inject constructor(
).also { source -> ).also { source ->
currentSource = source currentSource = source
} }
}, }
).flow.cachedIn(viewModelScope) ).flow.cachedIn(viewModelScope)
fun searchAutocompleteSuggestions(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
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"
}
} }

View File

@ -35,4 +35,13 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior"
/> />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="@string/action_mention"
app:srcCompat="@drawable/ic_hashtag" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
<!-- textNoSuggestions is to disable spell check, it will auto-complete -->
<AutoCompleteTextView
android:id="@+id/hashtag"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textNoSuggestions"
android:hint="@string/dialog_follow_hashtag_hint" />
</LinearLayout>

View File

@ -58,6 +58,9 @@
<string name="title_followed_hashtags">Followed hashtags</string> <string name="title_followed_hashtags">Followed hashtags</string>
<string name="title_edits">Edits</string> <string name="title_edits">Edits</string>
<string name="dialog_follow_hashtag_title">Follow hashtag</string>
<string name="dialog_follow_hashtag_hint">#hashtag</string>
<string name="post_username_format">\@%s</string> <string name="post_username_format">\@%s</string>
<string name="post_boosted_format">%s boosted</string> <string name="post_boosted_format">%s boosted</string>
<string name="post_sensitive_media_title">Sensitive content</string> <string name="post_sensitive_media_title">Sensitive content</string>