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
|
@ -133,6 +133,7 @@
|
|||
<activity android:name=".ListsActivity" />
|
||||
<activity android:name=".LicenseActivity" />
|
||||
<activity android:name=".FiltersActivity" />
|
||||
<activity android:name=".components.followedtags.FollowedTagsActivity" />
|
||||
<activity
|
||||
android:name=".components.report.ReportActivity"
|
||||
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.appstore.EventHub
|
||||
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.login.LoginActivity
|
||||
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 {
|
||||
setTitle(R.string.action_view_mutes)
|
||||
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.compose.ComposeActivity
|
||||
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.login.LoginActivity
|
||||
import com.keylesspalace.tusky.components.login.LoginWebViewActivity
|
||||
|
@ -103,6 +104,9 @@ abstract class ActivitiesModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract fun contributesFiltersActivity(): FiltersActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributesFollowedTagsActivity(): FollowedTagsActivity
|
||||
|
||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||
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.conversation.ConversationsViewModel
|
||||
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.report.ReportViewModel
|
||||
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusViewModel
|
||||
|
@ -126,5 +127,10 @@ abstract class ViewModelModule {
|
|||
@ViewModelKey(LoginWebViewViewModel::class)
|
||||
internal abstract fun loginWebViewViewModel(viewModel: LoginWebViewViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(FollowedTagsViewModel::class)
|
||||
internal abstract fun followedTagsViewModel(viewModel: FollowedTagsViewModel): ViewModel
|
||||
|
||||
// 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}")
|
||||
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")
|
||||
suspend fun followTag(@Path("name") name: String): NetworkResult<HashTag>
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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_following_hashtag_format">Error following #%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_home">Home</string>
|
||||
|
@ -50,6 +51,7 @@
|
|||
<string name="title_scheduled_posts">Scheduled posts</string>
|
||||
<string name="title_announcements">Announcements</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_boosted_format">%s boosted</string>
|
||||
|
@ -176,6 +178,7 @@
|
|||
<string name="confirmation_unblocked">User unblocked</string>
|
||||
<string name="confirmation_unmuted">User unmuted</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_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_title">%s rules</string>
|
||||
<string name="action_unfollow_hashtag_format">Unfollow #%s?</string>
|
||||
|
||||
</resources>
|
||||
|
|
Loading…
Reference in New Issue