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
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,4 +35,13 @@
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
<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_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>
|
||||||
|
|
Loading…
Reference in a new issue