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:
parent
41d493e72a
commit
27f6976295
5 changed files with 123 additions and 6 deletions
|
@ -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<ComposeAutoCompleteAdapter.AutocompleteResult> {
|
||||
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<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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<HashTag> = 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<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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,4 +35,13 @@
|
|||
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>
|
16
app/src/main/res/layout/dialog_follow_hashtag.xml
Normal file
16
app/src/main/res/layout/dialog_follow_hashtag.xml
Normal 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>
|
|
@ -58,6 +58,9 @@
|
|||
<string name="title_followed_hashtags">Followed hashtags</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_boosted_format">%s boosted</string>
|
||||
<string name="post_sensitive_media_title">Sensitive content</string>
|
||||
|
|
Loading…
Reference in a new issue