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
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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>
|
||||||
}
|
}
|
||||||
|
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_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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user