Skip to content

Commit

Permalink
Add FAB to follow new hashtags from FollowedTagsActivity (#3275)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
Nik Clayton authored Feb 20, 2023
1 parent 41d493e commit 27f6976
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 }

Expand All @@ -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)

Expand All @@ -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 {
Expand All @@ -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()
},
{
Expand Down Expand Up @@ -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()
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"
}
}
11 changes: 10 additions & 1 deletion app/src/main/res/layout/activity_followed_tags.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,13 @@
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 changes: 16 additions & 0 deletions app/src/main/res/layout/dialog_follow_hashtag.xml
Original file line number Diff line number Diff line change
@@ -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>
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down

0 comments on commit 27f6976

Please sign in to comment.