Add support for following hashtags (#2642)
* Add support for following hashtags. Addresses #2637 * Update rxjava to coroutines * Update new tag api to use suspend functions * Update hashtag unfollow icon * Set correct tint on hashtag follow/unfollow icons * Translate hashtag follow/unfollow error messages * Toast => Snackbar * Remove unnecessary view lookup
This commit is contained in:
parent
93d5cb1e0c
commit
042176e523
6 changed files with 123 additions and 4 deletions
|
@ -18,12 +18,20 @@ package com.keylesspalace.tusky
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import at.connyduck.calladapter.networkresult.fold
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel.Kind
|
||||
import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||
|
@ -31,16 +39,21 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
@Inject
|
||||
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
|
||||
|
||||
private val binding: ActivityStatuslistBinding by viewBinding(ActivityStatuslistBinding::inflate)
|
||||
private lateinit var kind: Kind
|
||||
private var hashtag: String? = null
|
||||
private var followTagItem: MenuItem? = null
|
||||
private var unfollowTagItem: MenuItem? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val binding = ActivityStatuslistBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||
|
||||
val kind = Kind.valueOf(intent.getStringExtra(EXTRA_KIND)!!)
|
||||
kind = Kind.valueOf(intent.getStringExtra(EXTRA_KIND)!!)
|
||||
val listId = intent.getStringExtra(EXTRA_LIST_ID)
|
||||
val hashtag = intent.getStringExtra(EXTRA_HASHTAG)
|
||||
hashtag = intent.getStringExtra(EXTRA_HASHTAG)
|
||||
|
||||
val title = when (kind) {
|
||||
Kind.FAVOURITES -> getString(R.string.title_favourites)
|
||||
|
@ -67,6 +80,70 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
val tag = hashtag
|
||||
if (kind == Kind.TAG && tag != null) {
|
||||
lifecycleScope.launch {
|
||||
mastodonApi.tag(tag).fold(
|
||||
{ tagEntity ->
|
||||
menuInflater.inflate(R.menu.view_hashtag_toolbar, menu)
|
||||
followTagItem = menu.findItem(R.id.action_follow_hashtag)
|
||||
unfollowTagItem = menu.findItem(R.id.action_unfollow_hashtag)
|
||||
followTagItem?.isVisible = tagEntity.following == false
|
||||
unfollowTagItem?.isVisible = tagEntity.following == true
|
||||
followTagItem?.setOnMenuItemClickListener { followTag() }
|
||||
unfollowTagItem?.setOnMenuItemClickListener { unfollowTag() }
|
||||
},
|
||||
{
|
||||
Log.w(TAG, "Failed to query tag #$tag", it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
private fun followTag(): Boolean {
|
||||
val tag = hashtag
|
||||
if (tag != null) {
|
||||
lifecycleScope.launch {
|
||||
mastodonApi.followTag(tag).fold(
|
||||
{
|
||||
followTagItem?.isVisible = false
|
||||
unfollowTagItem?.isVisible = true
|
||||
},
|
||||
{
|
||||
Snackbar.make(binding.root, getString(R.string.error_following_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
|
||||
Log.e(TAG, "Failed to follow #$tag", it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun unfollowTag(): Boolean {
|
||||
val tag = hashtag
|
||||
if (tag != null) {
|
||||
lifecycleScope.launch {
|
||||
mastodonApi.unfollowTag(tag).fold(
|
||||
{
|
||||
followTagItem?.isVisible = true
|
||||
unfollowTagItem?.isVisible = false
|
||||
},
|
||||
{
|
||||
Snackbar.make(binding.root, getString(R.string.error_unfollowing_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
|
||||
Log.e(TAG, "Failed to unfollow #$tag", it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun androidInjector() = dispatchingAndroidInjector
|
||||
|
||||
companion object {
|
||||
|
@ -75,6 +152,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
private const val EXTRA_LIST_ID = "id"
|
||||
private const val EXTRA_LIST_TITLE = "title"
|
||||
private const val EXTRA_HASHTAG = "tag"
|
||||
const val TAG = "StatusListActivity"
|
||||
|
||||
fun newFavouritesIntent(context: Context) =
|
||||
Intent(context, StatusListActivity::class.java).apply {
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
data class HashTag(val name: String, val url: String)
|
||||
data class HashTag(val name: String, val url: String, val following: Boolean? = null)
|
||||
|
|
|
@ -25,6 +25,7 @@ import com.keylesspalace.tusky.entity.Conversation
|
|||
import com.keylesspalace.tusky.entity.DeletedStatus
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.Filter
|
||||
import com.keylesspalace.tusky.entity.HashTag
|
||||
import com.keylesspalace.tusky.entity.Instance
|
||||
import com.keylesspalace.tusky.entity.Marker
|
||||
import com.keylesspalace.tusky.entity.MastoList
|
||||
|
@ -656,4 +657,13 @@ interface MastodonApi {
|
|||
@Header("Authorization") auth: String,
|
||||
@Header(DOMAIN_HEADER) domain: String,
|
||||
): NetworkResult<ResponseBody>
|
||||
|
||||
@GET("api/v1/tags/{name}")
|
||||
suspend fun tag(@Path("name") name: String): NetworkResult<HashTag>
|
||||
|
||||
@POST("api/v1/tags/{name}/follow")
|
||||
suspend fun followTag(@Path("name") name: String): NetworkResult<HashTag>
|
||||
|
||||
@POST("api/v1/tags/{name}/unfollow")
|
||||
suspend fun unfollowTag(@Path("name") name: String): NetworkResult<HashTag>
|
||||
}
|
||||
|
|
8
app/src/main/res/drawable/ic_person_remove_24dp.xml
Normal file
8
app/src/main/res/drawable/ic_person_remove_24dp.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<!-- drawable/account_remove.xml -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path android:fillColor="#000" android:pathData="M15,14C17.67,14 23,15.33 23,18V20H7V18C7,15.33 12.33,14 15,14M15,12A4,4 0 0,1 11,8A4,4 0 0,1 15,4A4,4 0 0,1 19,8A4,4 0 0,1 15,12M5,9.59L7.12,7.46L8.54,8.88L6.41,11L8.54,13.12L7.12,14.54L5,12.41L2.88,14.54L1.46,13.12L3.59,11L1.46,8.88L2.88,7.46L5,9.59Z" />
|
||||
</vector>
|
21
app/src/main/res/menu/view_hashtag_toolbar.xml
Normal file
21
app/src/main/res/menu/view_hashtag_toolbar.xml
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_follow_hashtag"
|
||||
android:title="@string/action_follow"
|
||||
app:showAsAction="ifRoom"
|
||||
app:iconTint="?attr/colorOnSurface"
|
||||
android:icon="@drawable/ic_person_add_24dp" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_unfollow_hashtag"
|
||||
android:title="@string/action_unfollow"
|
||||
app:showAsAction="ifRoom"
|
||||
app:iconTint="?attr/colorOnSurface"
|
||||
android:icon="@drawable/ic_person_remove_24dp" />
|
||||
|
||||
|
||||
</menu>
|
|
@ -22,6 +22,8 @@
|
|||
<string name="error_media_upload_image_or_video">Images and videos cannot both be attached to the same post.</string>
|
||||
<string name="error_media_upload_sending">The upload failed.</string>
|
||||
<string name="error_sender_account_gone">Error sending post.</string>
|
||||
<string name="error_following_hashtag_format">Error following #%s</string>
|
||||
<string name="error_unfollowing_hashtag_format">Error unfollowing #%s</string>
|
||||
|
||||
<string name="title_login">Login</string>
|
||||
<string name="title_home">Home</string>
|
||||
|
|
Loading…
Reference in a new issue