Support setting filter expirations (#2667)
* Show filter expiration in list * Add support for setting and updating the duration of a filter * Add tests for duration conversion math * Refactor network wrapper code * Mark updated mastodon api functions as suspend * Avoid creating unnecessary Date objects * Apply suggestions to filter dialog layout
This commit is contained in:
parent
9beea540de
commit
c47d9ef6ac
|
@ -1,26 +1,25 @@
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.format.DateUtils
|
||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
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.databinding.ActivityFiltersBinding
|
import com.keylesspalace.tusky.databinding.ActivityFiltersBinding
|
||||||
import com.keylesspalace.tusky.databinding.DialogFilterBinding
|
|
||||||
import com.keylesspalace.tusky.entity.Filter
|
import com.keylesspalace.tusky.entity.Filter
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
import com.keylesspalace.tusky.view.getSecondsForDurationIndex
|
||||||
|
import com.keylesspalace.tusky.view.setupEditDialogForFilter
|
||||||
|
import com.keylesspalace.tusky.view.showAddFilterDialog
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.await
|
import kotlinx.coroutines.rx3.await
|
||||||
import okhttp3.ResponseBody
|
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.Callback
|
|
||||||
import retrofit2.Response
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -47,7 +46,7 @@ class FiltersActivity : BaseActivity() {
|
||||||
setDisplayShowHomeEnabled(true)
|
setDisplayShowHomeEnabled(true)
|
||||||
}
|
}
|
||||||
binding.addFilterButton.setOnClickListener {
|
binding.addFilterButton.setOnClickListener {
|
||||||
showAddFilterDialog()
|
showAddFilterDialog(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
title = intent?.getStringExtra(FILTERS_TITLE)
|
title = intent?.getStringExtra(FILTERS_TITLE)
|
||||||
|
@ -55,15 +54,10 @@ class FiltersActivity : BaseActivity() {
|
||||||
loadFilters()
|
loadFilters()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateFilter(filter: Filter, itemIndex: Int) {
|
fun updateFilter(id: String, phrase: String, filterContext: List<String>, irreversible: Boolean, wholeWord: Boolean, expiresInSeconds: Int?, itemIndex: Int) {
|
||||||
api.updateFilter(filter.id, filter.phrase, filter.context, filter.irreversible, filter.wholeWord, null)
|
lifecycleScope.launch {
|
||||||
.enqueue(object : Callback<Filter> {
|
api.updateFilter(id, phrase, filterContext, irreversible, wholeWord, expiresInSeconds).fold(
|
||||||
override fun onFailure(call: Call<Filter>, t: Throwable) {
|
{ updatedFilter ->
|
||||||
Toast.makeText(this@FiltersActivity, "Error updating filter '${filter.phrase}'", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponse(call: Call<Filter>, response: Response<Filter>) {
|
|
||||||
val updatedFilter = response.body()!!
|
|
||||||
if (updatedFilter.context.contains(context)) {
|
if (updatedFilter.context.contains(context)) {
|
||||||
filters[itemIndex] = updatedFilter
|
filters[itemIndex] = updatedFilter
|
||||||
} else {
|
} else {
|
||||||
|
@ -71,25 +65,30 @@ class FiltersActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
refreshFilterDisplay()
|
refreshFilterDisplay()
|
||||||
eventHub.dispatch(PreferenceChangedEvent(context))
|
eventHub.dispatch(PreferenceChangedEvent(context))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Toast.makeText(this@FiltersActivity, "Error updating filter '$phrase'", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteFilter(itemIndex: Int) {
|
fun deleteFilter(itemIndex: Int) {
|
||||||
val filter = filters[itemIndex]
|
val filter = filters[itemIndex]
|
||||||
if (filter.context.size == 1) {
|
if (filter.context.size == 1) {
|
||||||
|
lifecycleScope.launch {
|
||||||
// This is the only context for this filter; delete it
|
// This is the only context for this filter; delete it
|
||||||
api.deleteFilter(filters[itemIndex].id).enqueue(object : Callback<ResponseBody> {
|
api.deleteFilter(filters[itemIndex].id).fold(
|
||||||
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
|
{
|
||||||
Toast.makeText(this@FiltersActivity, "Error updating filter '${filters[itemIndex].phrase}'", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
|
|
||||||
filters.removeAt(itemIndex)
|
filters.removeAt(itemIndex)
|
||||||
refreshFilterDisplay()
|
refreshFilterDisplay()
|
||||||
eventHub.dispatch(PreferenceChangedEvent(context))
|
eventHub.dispatch(PreferenceChangedEvent(context))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Toast.makeText(this@FiltersActivity, "Error deleting filter '${filters[itemIndex].phrase}'", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
// Keep the filter, but remove it from this context
|
// Keep the filter, but remove it from this context
|
||||||
val oldFilter = filters[itemIndex]
|
val oldFilter = filters[itemIndex]
|
||||||
|
@ -97,69 +96,50 @@ class FiltersActivity : BaseActivity() {
|
||||||
oldFilter.id, oldFilter.phrase, oldFilter.context.filter { c -> c != context },
|
oldFilter.id, oldFilter.phrase, oldFilter.context.filter { c -> c != context },
|
||||||
oldFilter.expiresAt, oldFilter.irreversible, oldFilter.wholeWord
|
oldFilter.expiresAt, oldFilter.irreversible, oldFilter.wholeWord
|
||||||
)
|
)
|
||||||
updateFilter(newFilter, itemIndex)
|
updateFilter(
|
||||||
|
newFilter.id, newFilter.phrase, newFilter.context, newFilter.irreversible, newFilter.wholeWord,
|
||||||
|
getSecondsForDurationIndex(-1, this, oldFilter.expiresAt), itemIndex
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createFilter(phrase: String, wholeWord: Boolean) {
|
fun createFilter(phrase: String, wholeWord: Boolean, expiresInSeconds: Int? = null) {
|
||||||
api.createFilter(phrase, listOf(context), false, wholeWord, null).enqueue(object : Callback<Filter> {
|
lifecycleScope.launch {
|
||||||
override fun onResponse(call: Call<Filter>, response: Response<Filter>) {
|
api.createFilter(phrase, listOf(context), false, wholeWord, expiresInSeconds).fold(
|
||||||
val filterResponse = response.body()
|
{ filter ->
|
||||||
if (response.isSuccessful && filterResponse != null) {
|
filters.add(filter)
|
||||||
filters.add(filterResponse)
|
|
||||||
refreshFilterDisplay()
|
refreshFilterDisplay()
|
||||||
eventHub.dispatch(PreferenceChangedEvent(context))
|
eventHub.dispatch(PreferenceChangedEvent(context))
|
||||||
} else {
|
},
|
||||||
|
{
|
||||||
Toast.makeText(this@FiltersActivity, "Error creating filter '$phrase'", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@FiltersActivity, "Error creating filter '$phrase'", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<Filter>, t: Throwable) {
|
|
||||||
Toast.makeText(this@FiltersActivity, "Error creating filter '$phrase'", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showAddFilterDialog() {
|
|
||||||
val binding = DialogFilterBinding.inflate(layoutInflater)
|
|
||||||
binding.phraseWholeWord.isChecked = true
|
|
||||||
AlertDialog.Builder(this@FiltersActivity)
|
|
||||||
.setTitle(R.string.filter_addition_dialog_title)
|
|
||||||
.setView(binding.root)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
createFilter(binding.phraseEditText.text.toString(), binding.phraseWholeWord.isChecked)
|
|
||||||
}
|
|
||||||
.setNeutralButton(android.R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupEditDialogForItem(itemIndex: Int) {
|
|
||||||
val binding = DialogFilterBinding.inflate(layoutInflater)
|
|
||||||
val filter = filters[itemIndex]
|
|
||||||
binding.phraseEditText.setText(filter.phrase)
|
|
||||||
binding.phraseWholeWord.isChecked = filter.wholeWord
|
|
||||||
|
|
||||||
AlertDialog.Builder(this@FiltersActivity)
|
|
||||||
.setTitle(R.string.filter_edit_dialog_title)
|
|
||||||
.setView(binding.root)
|
|
||||||
.setPositiveButton(R.string.filter_dialog_update_button) { _, _ ->
|
|
||||||
val oldFilter = filters[itemIndex]
|
|
||||||
val newFilter = Filter(
|
|
||||||
oldFilter.id, binding.phraseEditText.text.toString(), oldFilter.context,
|
|
||||||
oldFilter.expiresAt, oldFilter.irreversible, binding.phraseWholeWord.isChecked
|
|
||||||
)
|
)
|
||||||
updateFilter(newFilter, itemIndex)
|
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.filter_dialog_remove_button) { _, _ ->
|
|
||||||
deleteFilter(itemIndex)
|
|
||||||
}
|
|
||||||
.setNeutralButton(android.R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshFilterDisplay() {
|
private fun refreshFilterDisplay() {
|
||||||
binding.filtersView.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, filters.map { filter -> filter.phrase })
|
binding.filtersView.adapter = ArrayAdapter(
|
||||||
binding.filtersView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> setupEditDialogForItem(position) }
|
this,
|
||||||
|
android.R.layout.simple_list_item_1,
|
||||||
|
filters.map { filter ->
|
||||||
|
if (filter.expiresAt == null) {
|
||||||
|
filter.phrase
|
||||||
|
} else {
|
||||||
|
getString(
|
||||||
|
R.string.filter_expiration_format,
|
||||||
|
filter.phrase,
|
||||||
|
DateUtils.getRelativeTimeSpanString(
|
||||||
|
filter.expiresAt.time,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
DateUtils.MINUTE_IN_MILLIS,
|
||||||
|
DateUtils.FORMAT_ABBREV_RELATIVE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
binding.filtersView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> setupEditDialogForFilter(this, filters[position], position) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFilters() {
|
private fun loadFilters() {
|
||||||
|
|
|
@ -531,29 +531,29 @@ interface MastodonApi {
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("api/v1/filters")
|
@POST("api/v1/filters")
|
||||||
fun createFilter(
|
suspend fun createFilter(
|
||||||
@Field("phrase") phrase: String,
|
@Field("phrase") phrase: String,
|
||||||
@Field("context[]") context: List<String>,
|
@Field("context[]") context: List<String>,
|
||||||
@Field("irreversible") irreversible: Boolean?,
|
@Field("irreversible") irreversible: Boolean?,
|
||||||
@Field("whole_word") wholeWord: Boolean?,
|
@Field("whole_word") wholeWord: Boolean?,
|
||||||
@Field("expires_in") expiresIn: Int?
|
@Field("expires_in") expiresInSeconds: Int?
|
||||||
): Call<Filter>
|
): NetworkResult<Filter>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@PUT("api/v1/filters/{id}")
|
@PUT("api/v1/filters/{id}")
|
||||||
fun updateFilter(
|
suspend fun updateFilter(
|
||||||
@Path("id") id: String,
|
@Path("id") id: String,
|
||||||
@Field("phrase") phrase: String,
|
@Field("phrase") phrase: String,
|
||||||
@Field("context[]") context: List<String>,
|
@Field("context[]") context: List<String>,
|
||||||
@Field("irreversible") irreversible: Boolean?,
|
@Field("irreversible") irreversible: Boolean?,
|
||||||
@Field("whole_word") wholeWord: Boolean?,
|
@Field("whole_word") wholeWord: Boolean?,
|
||||||
@Field("expires_in") expiresIn: Int?
|
@Field("expires_in") expiresInSeconds: Int?
|
||||||
): Call<Filter>
|
): NetworkResult<Filter>
|
||||||
|
|
||||||
@DELETE("api/v1/filters/{id}")
|
@DELETE("api/v1/filters/{id}")
|
||||||
fun deleteFilter(
|
suspend fun deleteFilter(
|
||||||
@Path("id") id: String
|
@Path("id") id: String
|
||||||
): Call<ResponseBody>
|
): NetworkResult<ResponseBody>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("api/v1/polls/{id}/votes")
|
@POST("api/v1/polls/{id}/votes")
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
package com.keylesspalace.tusky.view
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.keylesspalace.tusky.FiltersActivity
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.databinding.DialogFilterBinding
|
||||||
|
import com.keylesspalace.tusky.entity.Filter
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
fun showAddFilterDialog(activity: FiltersActivity) {
|
||||||
|
val binding = DialogFilterBinding.inflate(activity.layoutInflater)
|
||||||
|
binding.phraseWholeWord.isChecked = true
|
||||||
|
binding.filterDurationSpinner.adapter = ArrayAdapter(
|
||||||
|
activity,
|
||||||
|
android.R.layout.simple_list_item_1,
|
||||||
|
activity.resources.getStringArray(R.array.filter_duration_names)
|
||||||
|
)
|
||||||
|
AlertDialog.Builder(activity)
|
||||||
|
.setTitle(R.string.filter_addition_dialog_title)
|
||||||
|
.setView(binding.root)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
activity.createFilter(
|
||||||
|
binding.phraseEditText.text.toString(), binding.phraseWholeWord.isChecked,
|
||||||
|
getSecondsForDurationIndex(binding.filterDurationSpinner.selectedItemPosition, activity)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.setNeutralButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setupEditDialogForFilter(activity: FiltersActivity, filter: Filter, itemIndex: Int) {
|
||||||
|
val binding = DialogFilterBinding.inflate(activity.layoutInflater)
|
||||||
|
binding.phraseEditText.setText(filter.phrase)
|
||||||
|
binding.phraseWholeWord.isChecked = filter.wholeWord
|
||||||
|
val filterNames = activity.resources.getStringArray(R.array.filter_duration_names).toMutableList()
|
||||||
|
if (filter.expiresAt != null) {
|
||||||
|
filterNames.add(0, activity.getString(R.string.duration_no_change))
|
||||||
|
}
|
||||||
|
binding.filterDurationSpinner.adapter = ArrayAdapter(activity, android.R.layout.simple_list_item_1, filterNames)
|
||||||
|
|
||||||
|
AlertDialog.Builder(activity)
|
||||||
|
.setTitle(R.string.filter_edit_dialog_title)
|
||||||
|
.setView(binding.root)
|
||||||
|
.setPositiveButton(R.string.filter_dialog_update_button) { _, _ ->
|
||||||
|
var index = binding.filterDurationSpinner.selectedItemPosition
|
||||||
|
if (filter.expiresAt != null) {
|
||||||
|
// We prepended "No changes", account for that here
|
||||||
|
--index
|
||||||
|
}
|
||||||
|
activity.updateFilter(
|
||||||
|
filter.id, binding.phraseEditText.text.toString(), filter.context,
|
||||||
|
filter.irreversible, binding.phraseWholeWord.isChecked,
|
||||||
|
getSecondsForDurationIndex(index, activity, filter.expiresAt), itemIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.filter_dialog_remove_button) { _, _ ->
|
||||||
|
activity.deleteFilter(itemIndex)
|
||||||
|
}
|
||||||
|
.setNeutralButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mastodon *stores* the absolute date in the filter,
|
||||||
|
// but create/edit take a number of seconds (relative to the time the operation is posted)
|
||||||
|
fun getSecondsForDurationIndex(index: Int, context: Context?, default: Date? = null): Int? {
|
||||||
|
return when (index) {
|
||||||
|
-1 -> if (default == null) { default } else { ((default.time - System.currentTimeMillis()) / 1000).toInt() }
|
||||||
|
0 -> null
|
||||||
|
else -> context?.resources?.getIntArray(R.array.filter_duration_values)?.get(index)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,32 +4,34 @@
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:paddingTop="16dp">
|
android:padding="24dp">
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/phraseEditText"
|
android:id="@+id/phraseEditText"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingEnd="24dp"
|
|
||||||
android:paddingStart="24dp"
|
|
||||||
android:hint="@string/filter_add_description"
|
android:hint="@string/filter_add_description"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
/>
|
/>
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/filterDurationSpinner"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/phraseEditText"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
/>
|
||||||
<CheckBox
|
<CheckBox
|
||||||
android:id="@+id/phraseWholeWord"
|
android:id="@+id/phraseWholeWord"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingEnd="24dp"
|
|
||||||
android:paddingStart="24dp"
|
|
||||||
android:text="@string/filter_dialog_whole_word"
|
android:text="@string/filter_dialog_whole_word"
|
||||||
app:layout_constraintTop_toBottomOf="@id/phraseEditText"
|
app:layout_constraintTop_toBottomOf="@id/filterDurationSpinner"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
/>
|
/>
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingEnd="8dp"
|
android:lineSpacingMultiplier="1.1"
|
||||||
android:paddingStart="8dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/phraseWholeWord"
|
app:layout_constraintTop_toBottomOf="@id/phraseWholeWord"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
android:text="@string/filter_dialog_whole_word_description"
|
android:text="@string/filter_dialog_whole_word_description"
|
||||||
|
|
|
@ -190,6 +190,8 @@
|
||||||
<item>31536000</item>
|
<item>31536000</item>
|
||||||
</integer-array>
|
</integer-array>
|
||||||
|
|
||||||
|
<string name="poll_percent_format"><!-- 15% --> <b>%1$d%%</b></string>
|
||||||
|
|
||||||
<string-array name="mute_duration_names">
|
<string-array name="mute_duration_names">
|
||||||
<item>@string/duration_indefinite</item>
|
<item>@string/duration_indefinite</item>
|
||||||
<item>@string/duration_5_min</item>
|
<item>@string/duration_5_min</item>
|
||||||
|
@ -212,5 +214,25 @@
|
||||||
<item>604800</item>
|
<item>604800</item>
|
||||||
</integer-array>
|
</integer-array>
|
||||||
|
|
||||||
<string name="poll_percent_format"><!-- 15% --> <b>%1$d%%</b></string>
|
<string-array name="filter_duration_names">
|
||||||
|
<item>@string/duration_indefinite</item>
|
||||||
|
<item>@string/duration_5_min</item>
|
||||||
|
<item>@string/duration_30_min</item>
|
||||||
|
<item>@string/duration_1_hour</item>
|
||||||
|
<item>@string/duration_6_hours</item>
|
||||||
|
<item>@string/duration_1_day</item>
|
||||||
|
<item>@string/duration_3_days</item>
|
||||||
|
<item>@string/duration_7_days</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<integer-array name="filter_duration_values"> <!-- values in seconds, corresponding to mute_duration_names -->
|
||||||
|
<item>0</item>
|
||||||
|
<item>300</item>
|
||||||
|
<item>1800</item>
|
||||||
|
<item>3600</item>
|
||||||
|
<item>21600</item>
|
||||||
|
<item>86400</item>
|
||||||
|
<item>259200</item>
|
||||||
|
<item>604800</item>
|
||||||
|
</integer-array>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -383,6 +383,7 @@
|
||||||
<string name="filter_dialog_whole_word">Whole word</string>
|
<string name="filter_dialog_whole_word">Whole word</string>
|
||||||
<string name="filter_dialog_whole_word_description">When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word</string>
|
<string name="filter_dialog_whole_word_description">When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word</string>
|
||||||
<string name="filter_add_description">Phrase to filter</string>
|
<string name="filter_add_description">Phrase to filter</string>
|
||||||
|
<string name="filter_expiration_format">%s (%s)</string>
|
||||||
|
|
||||||
<string name="add_account_name">Add Account</string>
|
<string name="add_account_name">Add Account</string>
|
||||||
<string name="add_account_description">Add new Mastodon Account</string>
|
<string name="add_account_description">Add new Mastodon Account</string>
|
||||||
|
@ -602,6 +603,7 @@
|
||||||
<string name="duration_90_days">90 days</string>
|
<string name="duration_90_days">90 days</string>
|
||||||
<string name="duration_180_days">180 days</string>
|
<string name="duration_180_days">180 days</string>
|
||||||
<string name="duration_365_days">365 days</string>
|
<string name="duration_365_days">365 days</string>
|
||||||
|
<string name="duration_no_change">(No change)</string>
|
||||||
<string name="add_poll_choice">Add choice</string>
|
<string name="add_poll_choice">Add choice</string>
|
||||||
<string name="poll_allow_multiple_choices">Multiple choices</string>
|
<string name="poll_allow_multiple_choices">Multiple choices</string>
|
||||||
<string name="poll_new_choice_hint">Choice %d</string>
|
<string name="poll_new_choice_hint">Choice %d</string>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.PollOption
|
import com.keylesspalace.tusky.entity.PollOption
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.FilterModel
|
import com.keylesspalace.tusky.network.FilterModel
|
||||||
|
import com.keylesspalace.tusky.view.getSecondsForDurationIndex
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
@ -182,6 +183,23 @@ class FilterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun unchangedExpiration_shouldBeNegative_whenFilterIsExpired() {
|
||||||
|
val expiredBySeconds = 3600
|
||||||
|
val expiredDate = Date.from(Instant.now().minusSeconds(expiredBySeconds.toLong()))
|
||||||
|
val updatedDuration = getSecondsForDurationIndex(-1, null, expiredDate)
|
||||||
|
assert(updatedDuration != null && updatedDuration <= -expiredBySeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun unchangedExpiration_shouldBePositive_whenFilterIsUnexpired() {
|
||||||
|
val expiresInSeconds = 3600
|
||||||
|
val expiredDate = Date.from(Instant.now().plusSeconds(expiresInSeconds.toLong()))
|
||||||
|
val updatedDuration = getSecondsForDurationIndex(-1, null, expiredDate)
|
||||||
|
assert(updatedDuration != null && updatedDuration > (expiresInSeconds - 60))
|
||||||
|
}
|
||||||
|
|
||||||
private fun mockStatus(
|
private fun mockStatus(
|
||||||
content: String = "",
|
content: String = "",
|
||||||
spoilerText: String = "",
|
spoilerText: String = "",
|
||||||
|
|
Loading…
Reference in New Issue