Add view for browsing and unfollowing followed hashtags (#2794)
* Add view for browsing and unfollowing followed hashtags. Implements #2785 * Improve list interface * Remove superfluous suspend modifier * Migrate to paginated loading for followed tags view * Update app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsViewModel.kt Co-authored-by: Konrad Pozniak <connyduck@users.noreply.github.com> * Fix unhandled exception when opening the followed tags view while offline Co-authored-by: Konrad Pozniak <connyduck@users.noreply.github.com>
This commit is contained in:
parent
b53f097d45
commit
9362e59d9d
14 changed files with 414 additions and 0 deletions
|
@ -133,6 +133,7 @@
|
||||||
<activity android:name=".ListsActivity" />
|
<activity android:name=".ListsActivity" />
|
||||||
<activity android:name=".LicenseActivity" />
|
<activity android:name=".LicenseActivity" />
|
||||||
<activity android:name=".FiltersActivity" />
|
<activity android:name=".FiltersActivity" />
|
||||||
|
<activity android:name=".components.followedtags.FollowedTagsActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".components.report.ReportActivity"
|
android:name=".components.report.ReportActivity"
|
||||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||||
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
package com.keylesspalace.tusky.components.followedtags
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.paging.LoadState
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
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.databinding.ActivityFollowedTagsBinding
|
||||||
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
|
import com.keylesspalace.tusky.interfaces.HashtagActionListener
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
import com.keylesspalace.tusky.util.visible
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.IOException
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class FollowedTagsActivity : BaseActivity(), HashtagActionListener {
|
||||||
|
@Inject
|
||||||
|
lateinit var api: MastodonApi
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var viewModelFactory: ViewModelFactory
|
||||||
|
|
||||||
|
private val binding by viewBinding(ActivityFollowedTagsBinding::inflate)
|
||||||
|
private val viewModel: FollowedTagsViewModel by viewModels { viewModelFactory }
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContentView(binding.root)
|
||||||
|
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||||
|
supportActionBar?.run {
|
||||||
|
setTitle(R.string.title_followed_hashtags)
|
||||||
|
// Back button
|
||||||
|
setDisplayHomeAsUpEnabled(true)
|
||||||
|
setDisplayShowHomeEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
setupAdapter().let { adapter ->
|
||||||
|
setupRecyclerView(adapter)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.pager.collectLatest { pagingData ->
|
||||||
|
adapter.submitData(pagingData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView(adapter: FollowedTagsAdapter) {
|
||||||
|
binding.followedTagsView.adapter = adapter
|
||||||
|
binding.followedTagsView.setHasFixedSize(true)
|
||||||
|
binding.followedTagsView.layoutManager = LinearLayoutManager(this)
|
||||||
|
binding.followedTagsView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
|
||||||
|
(binding.followedTagsView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupAdapter(): FollowedTagsAdapter {
|
||||||
|
return FollowedTagsAdapter(this, viewModel).apply {
|
||||||
|
addLoadStateListener { loadState ->
|
||||||
|
binding.followedTagsProgressBar.visible(loadState.refresh == LoadState.Loading && itemCount == 0)
|
||||||
|
|
||||||
|
if (loadState.refresh is LoadState.Error) {
|
||||||
|
binding.followedTagsView.hide()
|
||||||
|
binding.followedTagsMessageView.show()
|
||||||
|
val errorState = loadState.refresh as LoadState.Error
|
||||||
|
if (errorState.error is IOException) {
|
||||||
|
binding.followedTagsMessageView.setup(R.drawable.elephant_offline, R.string.error_network) { retry() }
|
||||||
|
} else {
|
||||||
|
binding.followedTagsMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { retry() }
|
||||||
|
}
|
||||||
|
Log.w(TAG, "error loading followed hashtags", errorState.error)
|
||||||
|
} else {
|
||||||
|
binding.followedTagsView.show()
|
||||||
|
binding.followedTagsMessageView.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun follow(tagName: String, position: Int) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
api.followTag(tagName).fold(
|
||||||
|
{
|
||||||
|
viewModel.tags.add(position, it)
|
||||||
|
viewModel.currentSource?.invalidate()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Snackbar.make(
|
||||||
|
this@FollowedTagsActivity,
|
||||||
|
binding.followedTagsView,
|
||||||
|
getString(R.string.error_following_hashtag_format, tagName),
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unfollow(tagName: String, position: Int) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
api.unfollowTag(tagName).fold(
|
||||||
|
{
|
||||||
|
viewModel.tags.removeAt(position)
|
||||||
|
Snackbar.make(
|
||||||
|
this@FollowedTagsActivity,
|
||||||
|
binding.followedTagsView,
|
||||||
|
getString(R.string.confirmation_hashtag_unfollowed, tagName),
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
)
|
||||||
|
.setAction(R.string.action_undo) {
|
||||||
|
follow(tagName, position)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
viewModel.currentSource?.invalidate()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Snackbar.make(
|
||||||
|
this@FollowedTagsActivity,
|
||||||
|
binding.followedTagsView,
|
||||||
|
getString(
|
||||||
|
R.string.error_unfollowing_hashtag_format,
|
||||||
|
tagName
|
||||||
|
),
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "FollowedTagsActivity"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.keylesspalace.tusky.components.followedtags
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.paging.PagingDataAdapter
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.databinding.ItemFollowedHashtagBinding
|
||||||
|
import com.keylesspalace.tusky.interfaces.HashtagActionListener
|
||||||
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
|
|
||||||
|
class FollowedTagsAdapter(
|
||||||
|
private val actionListener: HashtagActionListener,
|
||||||
|
private val viewModel: FollowedTagsViewModel,
|
||||||
|
) : PagingDataAdapter<String, BindingHolder<ItemFollowedHashtagBinding>>(STRING_COMPARATOR) {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowedHashtagBinding> =
|
||||||
|
BindingHolder(ItemFollowedHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: BindingHolder<ItemFollowedHashtagBinding>, position: Int) {
|
||||||
|
viewModel.tags[position].let { tag ->
|
||||||
|
holder.itemView.findViewById<TextView>(R.id.followed_tag).text = tag.name
|
||||||
|
holder.itemView.findViewById<ImageButton>(R.id.followed_tag_unfollow).setOnClickListener {
|
||||||
|
actionListener.unfollow(tag.name, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = viewModel.tags.size
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val STRING_COMPARATOR = object : DiffUtil.ItemCallback<String>() {
|
||||||
|
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean = oldItem == newItem
|
||||||
|
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean = oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.keylesspalace.tusky.components.followedtags
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
|
||||||
|
class FollowedTagsPagingSource(private val viewModel: FollowedTagsViewModel) : PagingSource<String, String>() {
|
||||||
|
override fun getRefreshKey(state: PagingState<String, String>): String? = null
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<String>): LoadResult<String, String> {
|
||||||
|
return if (params is LoadParams.Refresh) {
|
||||||
|
LoadResult.Page(viewModel.tags.map { it.name }, null, viewModel.nextKey)
|
||||||
|
} else {
|
||||||
|
LoadResult.Page(emptyList(), null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package com.keylesspalace.tusky.components.followedtags
|
||||||
|
|
||||||
|
import androidx.paging.ExperimentalPagingApi
|
||||||
|
import androidx.paging.LoadType
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import androidx.paging.RemoteMediator
|
||||||
|
import com.keylesspalace.tusky.entity.HashTag
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.keylesspalace.tusky.util.HttpHeaderLink
|
||||||
|
import retrofit2.HttpException
|
||||||
|
import retrofit2.Response
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
|
class FollowedTagsRemoteMediator(
|
||||||
|
private val api: MastodonApi,
|
||||||
|
private val viewModel: FollowedTagsViewModel,
|
||||||
|
) : RemoteMediator<String, String>() {
|
||||||
|
override suspend fun load(
|
||||||
|
loadType: LoadType,
|
||||||
|
state: PagingState<String, String>
|
||||||
|
): MediatorResult {
|
||||||
|
return try {
|
||||||
|
val response = request(loadType)
|
||||||
|
?: return MediatorResult.Success(endOfPaginationReached = true)
|
||||||
|
|
||||||
|
return applyResponse(response)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
MediatorResult.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun request(loadType: LoadType): Response<List<HashTag>>? {
|
||||||
|
return when (loadType) {
|
||||||
|
LoadType.PREPEND -> null
|
||||||
|
LoadType.APPEND -> api.followedTags(maxId = viewModel.nextKey)
|
||||||
|
LoadType.REFRESH -> {
|
||||||
|
viewModel.nextKey = null
|
||||||
|
viewModel.tags.clear()
|
||||||
|
api.followedTags()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyResponse(response: Response<List<HashTag>>): MediatorResult {
|
||||||
|
val tags = response.body()
|
||||||
|
if (!response.isSuccessful || tags == null) {
|
||||||
|
return MediatorResult.Error(HttpException(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
val links = HttpHeaderLink.parse(response.headers()["Link"])
|
||||||
|
viewModel.nextKey = HttpHeaderLink.findByRelationType(links, "next")?.uri?.getQueryParameter("max_id")
|
||||||
|
viewModel.tags.addAll(tags)
|
||||||
|
viewModel.currentSource?.invalidate()
|
||||||
|
|
||||||
|
return MediatorResult.Success(endOfPaginationReached = viewModel.nextKey == null)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.keylesspalace.tusky.components.followedtags
|
||||||
|
|
||||||
|
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 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
|
||||||
|
) : ViewModel(), Injectable {
|
||||||
|
val tags: MutableList<HashTag> = mutableListOf()
|
||||||
|
var nextKey: String? = null
|
||||||
|
var currentSource: FollowedTagsPagingSource? = null
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
|
val pager = Pager(
|
||||||
|
config = PagingConfig(pageSize = 100),
|
||||||
|
remoteMediator = FollowedTagsRemoteMediator(api, this),
|
||||||
|
pagingSourceFactory = {
|
||||||
|
FollowedTagsPagingSource(
|
||||||
|
viewModel = this
|
||||||
|
).also { source ->
|
||||||
|
currentSource = source
|
||||||
|
}
|
||||||
|
},
|
||||||
|
).flow.cachedIn(viewModelScope)
|
||||||
|
}
|
|
@ -30,6 +30,7 @@ import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.TabPreferenceActivity
|
import com.keylesspalace.tusky.TabPreferenceActivity
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||||
|
import com.keylesspalace.tusky.components.followedtags.FollowedTagsActivity
|
||||||
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
||||||
import com.keylesspalace.tusky.components.login.LoginActivity
|
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||||
import com.keylesspalace.tusky.components.notifications.currentAccountNeedsMigration
|
import com.keylesspalace.tusky.components.notifications.currentAccountNeedsMigration
|
||||||
|
@ -95,6 +96,20 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preference {
|
||||||
|
setTitle(R.string.title_followed_hashtags)
|
||||||
|
setIcon(R.drawable.ic_hashtag)
|
||||||
|
setOnPreferenceClickListener {
|
||||||
|
val intent = Intent(context, FollowedTagsActivity::class.java)
|
||||||
|
activity?.startActivity(intent)
|
||||||
|
activity?.overridePendingTransition(
|
||||||
|
R.anim.slide_from_right,
|
||||||
|
R.anim.slide_to_left
|
||||||
|
)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
preference {
|
preference {
|
||||||
setTitle(R.string.action_view_mutes)
|
setTitle(R.string.action_view_mutes)
|
||||||
setIcon(R.drawable.ic_mute_24dp)
|
setIcon(R.drawable.ic_mute_24dp)
|
||||||
|
|
|
@ -31,6 +31,7 @@ import com.keylesspalace.tusky.components.account.AccountActivity
|
||||||
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
||||||
|
import com.keylesspalace.tusky.components.followedtags.FollowedTagsActivity
|
||||||
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
||||||
import com.keylesspalace.tusky.components.login.LoginActivity
|
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||||
import com.keylesspalace.tusky.components.login.LoginWebViewActivity
|
import com.keylesspalace.tusky.components.login.LoginWebViewActivity
|
||||||
|
@ -103,6 +104,9 @@ abstract class ActivitiesModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesFiltersActivity(): FiltersActivity
|
abstract fun contributesFiltersActivity(): FiltersActivity
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract fun contributesFollowedTagsActivity(): FollowedTagsActivity
|
||||||
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||||
abstract fun contributesReportActivity(): ReportActivity
|
abstract fun contributesReportActivity(): ReportActivity
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftsViewModel
|
import com.keylesspalace.tusky.components.drafts.DraftsViewModel
|
||||||
|
import com.keylesspalace.tusky.components.followedtags.FollowedTagsViewModel
|
||||||
import com.keylesspalace.tusky.components.login.LoginWebViewViewModel
|
import com.keylesspalace.tusky.components.login.LoginWebViewViewModel
|
||||||
import com.keylesspalace.tusky.components.report.ReportViewModel
|
import com.keylesspalace.tusky.components.report.ReportViewModel
|
||||||
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusViewModel
|
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusViewModel
|
||||||
|
@ -126,5 +127,10 @@ abstract class ViewModelModule {
|
||||||
@ViewModelKey(LoginWebViewViewModel::class)
|
@ViewModelKey(LoginWebViewViewModel::class)
|
||||||
internal abstract fun loginWebViewViewModel(viewModel: LoginWebViewViewModel): ViewModel
|
internal abstract fun loginWebViewViewModel(viewModel: LoginWebViewViewModel): ViewModel
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ViewModelKey(FollowedTagsViewModel::class)
|
||||||
|
internal abstract fun followedTagsViewModel(viewModel: FollowedTagsViewModel): ViewModel
|
||||||
|
|
||||||
// Add more ViewModels here
|
// Add more ViewModels here
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.keylesspalace.tusky.interfaces
|
||||||
|
|
||||||
|
interface HashtagActionListener {
|
||||||
|
fun unfollow(tagName: String, position: Int)
|
||||||
|
}
|
|
@ -668,6 +668,14 @@ interface MastodonApi {
|
||||||
@GET("api/v1/tags/{name}")
|
@GET("api/v1/tags/{name}")
|
||||||
suspend fun tag(@Path("name") name: String): NetworkResult<HashTag>
|
suspend fun tag(@Path("name") name: String): NetworkResult<HashTag>
|
||||||
|
|
||||||
|
@GET("api/v1/followed_tags")
|
||||||
|
suspend fun followedTags(
|
||||||
|
@Query("min_id") minId: String? = null,
|
||||||
|
@Query("since_id") sinceId: String? = null,
|
||||||
|
@Query("max_id") maxId: String? = null,
|
||||||
|
@Query("limit") limit: Int? = null,
|
||||||
|
): Response<List<HashTag>>
|
||||||
|
|
||||||
@POST("api/v1/tags/{name}/follow")
|
@POST("api/v1/tags/{name}/follow")
|
||||||
suspend fun followTag(@Path("name") name: String): NetworkResult<HashTag>
|
suspend fun followTag(@Path("name") name: String): NetworkResult<HashTag>
|
||||||
|
|
||||||
|
|
38
app/src/main/res/layout/activity_followed_tags.xml
Normal file
38
app/src/main/res/layout/activity_followed_tags.xml
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context="com.keylesspalace.tusky.components.followedtags.FollowedTagsActivity">
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/includedToolbar"
|
||||||
|
layout="@layout/toolbar_basic" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/followedTagsView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:itemCount="5"
|
||||||
|
tools:listitem="@layout/item_followed_hashtag"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<com.keylesspalace.tusky.view.BackgroundMessageView
|
||||||
|
android:id="@+id/followedTagsMessageView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/followedTagsProgressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
41
app/src/main/res/layout/item_followed_hashtag.xml
Normal file
41
app/src/main/res/layout/item_followed_hashtag.xml
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="72dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingLeft="16dp"
|
||||||
|
android:paddingRight="16dp"
|
||||||
|
>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/followed_tag_unfollow"
|
||||||
|
style="@style/TuskyImageButton"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/action_unfollow"
|
||||||
|
android:padding="4dp"
|
||||||
|
app:srcCompat="@drawable/ic_person_remove_24dp"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/followed_tag"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="?android:textColorSecondary"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
tools:text="#hashtag" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -24,6 +24,7 @@
|
||||||
<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_following_hashtag_format">Error following #%s</string>
|
||||||
<string name="error_unfollowing_hashtag_format">Error unfollowing #%s</string>
|
<string name="error_unfollowing_hashtag_format">Error unfollowing #%s</string>
|
||||||
|
<string name="error_following_hashtags_unsupported">This instance does not support following hashtags.</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>
|
||||||
|
@ -50,6 +51,7 @@
|
||||||
<string name="title_scheduled_posts">Scheduled posts</string>
|
<string name="title_scheduled_posts">Scheduled posts</string>
|
||||||
<string name="title_announcements">Announcements</string>
|
<string name="title_announcements">Announcements</string>
|
||||||
<string name="title_licenses">Licenses</string>
|
<string name="title_licenses">Licenses</string>
|
||||||
|
<string name="title_followed_hashtags">Followed Hashtags</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>
|
||||||
|
@ -176,6 +178,7 @@
|
||||||
<string name="confirmation_unblocked">User unblocked</string>
|
<string name="confirmation_unblocked">User unblocked</string>
|
||||||
<string name="confirmation_unmuted">User unmuted</string>
|
<string name="confirmation_unmuted">User unmuted</string>
|
||||||
<string name="confirmation_domain_unmuted">%s unhidden</string>
|
<string name="confirmation_domain_unmuted">%s unhidden</string>
|
||||||
|
<string name="confirmation_hashtag_unfollowed">#%s unfollowed</string>
|
||||||
|
|
||||||
<string name="post_sent">Sent!</string>
|
<string name="post_sent">Sent!</string>
|
||||||
<string name="post_sent_long">Reply sent successfully.</string>
|
<string name="post_sent_long">Reply sent successfully.</string>
|
||||||
|
@ -676,5 +679,6 @@
|
||||||
|
|
||||||
<string name="instance_rule_info">By logging in you agree to the rules of %s.</string>
|
<string name="instance_rule_info">By logging in you agree to the rules of %s.</string>
|
||||||
<string name="instance_rule_title">%s rules</string>
|
<string name="instance_rule_title">%s rules</string>
|
||||||
|
<string name="action_unfollow_hashtag_format">Unfollow #%s?</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in a new issue