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:
Levi Bard 2022-08-07 19:09:26 +02:00 committed by GitHub
parent 93d5cb1e0c
commit 042176e523
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 123 additions and 4 deletions

View File

@ -18,12 +18,20 @@ package com.keylesspalace.tusky
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.fragment.app.commit 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.TimelineFragment
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel.Kind import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel.Kind
import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding
import com.keylesspalace.tusky.util.viewBinding
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
@ -31,16 +39,21 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
@Inject @Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any> 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val binding = ActivityStatuslistBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
setSupportActionBar(binding.includedToolbar.toolbar) 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 listId = intent.getStringExtra(EXTRA_LIST_ID)
val hashtag = intent.getStringExtra(EXTRA_HASHTAG) hashtag = intent.getStringExtra(EXTRA_HASHTAG)
val title = when (kind) { val title = when (kind) {
Kind.FAVOURITES -> getString(R.string.title_favourites) 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 override fun androidInjector() = dispatchingAndroidInjector
companion object { companion object {
@ -75,6 +152,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
private const val EXTRA_LIST_ID = "id" private const val EXTRA_LIST_ID = "id"
private const val EXTRA_LIST_TITLE = "title" private const val EXTRA_LIST_TITLE = "title"
private const val EXTRA_HASHTAG = "tag" private const val EXTRA_HASHTAG = "tag"
const val TAG = "StatusListActivity"
fun newFavouritesIntent(context: Context) = fun newFavouritesIntent(context: Context) =
Intent(context, StatusListActivity::class.java).apply { Intent(context, StatusListActivity::class.java).apply {

View File

@ -1,3 +1,3 @@
package com.keylesspalace.tusky.entity 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)

View File

@ -25,6 +25,7 @@ import com.keylesspalace.tusky.entity.Conversation
import com.keylesspalace.tusky.entity.DeletedStatus import com.keylesspalace.tusky.entity.DeletedStatus
import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.entity.Instance
import com.keylesspalace.tusky.entity.Marker import com.keylesspalace.tusky.entity.Marker
import com.keylesspalace.tusky.entity.MastoList import com.keylesspalace.tusky.entity.MastoList
@ -656,4 +657,13 @@ interface MastodonApi {
@Header("Authorization") auth: String, @Header("Authorization") auth: String,
@Header(DOMAIN_HEADER) domain: String, @Header(DOMAIN_HEADER) domain: String,
): NetworkResult<ResponseBody> ): 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>
} }

View 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>

View 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>

View File

@ -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_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_media_upload_sending">The upload failed.</string>
<string name="error_sender_account_gone">Error sending post.</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_login">Login</string>
<string name="title_home">Home</string> <string name="title_home">Home</string>