refactor: Remove duplicate strings from Filter contexts (#478)
A filter's context (previously referred to as its `kind`) controls where the filter is applied. This was implemented as an enum with a specific property to control how it would serialise when @FormUrlEncoded, and with a @Json annotation for Moshi. In addition, the model objects kept the filter context in its string form throughout Pachli, requiring periodic conversion to/from the enum type, making the code more complicated. Fix this, by: 1. Converting the incoming JSON value to the enum type immediately, so the rest of the code uses the enum constants exclusively. 2. Implement a Retrofit converter that serialises the enum value when @FormUrlEncoded to the same string used in JSON serialisation
This commit is contained in:
parent
2aa01fba8c
commit
9a23439d04
@ -33,6 +33,7 @@ import app.pachli.core.navigation.StatusListActivityIntent
|
|||||||
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_FILTERS_CLIENT
|
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_FILTERS_CLIENT
|
||||||
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_FILTERS_SERVER
|
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_FILTERS_SERVER
|
||||||
import app.pachli.core.network.model.Filter
|
import app.pachli.core.network.model.Filter
|
||||||
|
import app.pachli.core.network.model.FilterContext
|
||||||
import app.pachli.core.network.model.FilterV1
|
import app.pachli.core.network.model.FilterV1
|
||||||
import app.pachli.core.network.model.TimelineKind
|
import app.pachli.core.network.model.TimelineKind
|
||||||
import app.pachli.databinding.ActivityStatuslistBinding
|
import app.pachli.databinding.ActivityStatuslistBinding
|
||||||
@ -243,7 +244,7 @@ class StatusListActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButton
|
|||||||
mastodonApi.getFilters().fold(
|
mastodonApi.getFilters().fold(
|
||||||
{ filters ->
|
{ filters ->
|
||||||
mutedFilter = filters.firstOrNull { filter ->
|
mutedFilter = filters.firstOrNull { filter ->
|
||||||
filter.context.contains(Filter.Kind.HOME.kind) && filter.keywords.any {
|
filter.contexts.contains(FilterContext.HOME) && filter.keywords.any {
|
||||||
it.keyword == tagWithHash
|
it.keyword == tagWithHash
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -254,7 +255,7 @@ class StatusListActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButton
|
|||||||
mastodonApi.getFiltersV1().fold(
|
mastodonApi.getFiltersV1().fold(
|
||||||
{ filters ->
|
{ filters ->
|
||||||
mutedFilterV1 = filters.firstOrNull { filter ->
|
mutedFilterV1 = filters.firstOrNull { filter ->
|
||||||
tagWithHash == filter.phrase && filter.context.contains(FilterV1.HOME)
|
tagWithHash == filter.phrase && filter.contexts.contains(FilterContext.HOME)
|
||||||
}
|
}
|
||||||
updateTagMuteState(mutedFilterV1 != null)
|
updateTagMuteState(mutedFilterV1 != null)
|
||||||
},
|
},
|
||||||
@ -288,7 +289,7 @@ class StatusListActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButton
|
|||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
mastodonApi.createFilter(
|
mastodonApi.createFilter(
|
||||||
title = tagWithHash,
|
title = tagWithHash,
|
||||||
context = listOf(FilterV1.HOME),
|
context = listOf(FilterContext.HOME),
|
||||||
filterAction = Filter.Action.WARN.action,
|
filterAction = Filter.Action.WARN.action,
|
||||||
expiresInSeconds = null,
|
expiresInSeconds = null,
|
||||||
).fold(
|
).fold(
|
||||||
@ -296,7 +297,7 @@ class StatusListActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButton
|
|||||||
if (mastodonApi.addFilterKeyword(filterId = filter.id, keyword = tagWithHash, wholeWord = true).isSuccess) {
|
if (mastodonApi.addFilterKeyword(filterId = filter.id, keyword = tagWithHash, wholeWord = true).isSuccess) {
|
||||||
mutedFilter = filter
|
mutedFilter = filter
|
||||||
updateTagMuteState(true)
|
updateTagMuteState(true)
|
||||||
eventHub.dispatch(FilterChangedEvent(Filter.Kind.from(filter.context[0])))
|
eventHub.dispatch(FilterChangedEvent(filter.contexts[0]))
|
||||||
Snackbar.make(binding.root, getString(R.string.confirmation_hashtag_muted, hashtag), Snackbar.LENGTH_SHORT).show()
|
Snackbar.make(binding.root, getString(R.string.confirmation_hashtag_muted, hashtag), Snackbar.LENGTH_SHORT).show()
|
||||||
} else {
|
} else {
|
||||||
Snackbar.make(binding.root, getString(R.string.error_muting_hashtag_format, hashtag), Snackbar.LENGTH_SHORT).show()
|
Snackbar.make(binding.root, getString(R.string.error_muting_hashtag_format, hashtag), Snackbar.LENGTH_SHORT).show()
|
||||||
@ -307,7 +308,7 @@ class StatusListActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButton
|
|||||||
if (throwable is HttpException && throwable.code() == 404) {
|
if (throwable is HttpException && throwable.code() == 404) {
|
||||||
mastodonApi.createFilterV1(
|
mastodonApi.createFilterV1(
|
||||||
tagWithHash,
|
tagWithHash,
|
||||||
listOf(FilterV1.HOME),
|
listOf(FilterContext.HOME),
|
||||||
irreversible = false,
|
irreversible = false,
|
||||||
wholeWord = true,
|
wholeWord = true,
|
||||||
expiresInSeconds = null,
|
expiresInSeconds = null,
|
||||||
@ -315,7 +316,7 @@ class StatusListActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButton
|
|||||||
{ filter ->
|
{ filter ->
|
||||||
mutedFilterV1 = filter
|
mutedFilterV1 = filter
|
||||||
updateTagMuteState(true)
|
updateTagMuteState(true)
|
||||||
eventHub.dispatch(FilterChangedEvent(Filter.Kind.from(filter.context[0])))
|
eventHub.dispatch(FilterChangedEvent(filter.contexts[0]))
|
||||||
Snackbar.make(binding.root, getString(R.string.confirmation_hashtag_muted, hashtag), Snackbar.LENGTH_SHORT).show()
|
Snackbar.make(binding.root, getString(R.string.confirmation_hashtag_muted, hashtag), Snackbar.LENGTH_SHORT).show()
|
||||||
},
|
},
|
||||||
{ throwable ->
|
{ throwable ->
|
||||||
@ -340,23 +341,23 @@ class StatusListActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButton
|
|||||||
|
|
||||||
val result = if (mutedFilter != null) {
|
val result = if (mutedFilter != null) {
|
||||||
val filter = mutedFilter!!
|
val filter = mutedFilter!!
|
||||||
if (filter.context.size > 1) {
|
if (filter.contexts.size > 1) {
|
||||||
// This filter exists in multiple contexts, just remove the home context
|
// This filter exists in multiple contexts, just remove the home context
|
||||||
mastodonApi.updateFilter(
|
mastodonApi.updateFilter(
|
||||||
id = filter.id,
|
id = filter.id,
|
||||||
context = filter.context.filter { it != Filter.Kind.HOME.kind },
|
context = filter.contexts.filter { it != FilterContext.HOME },
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
mastodonApi.deleteFilter(filter.id)
|
mastodonApi.deleteFilter(filter.id)
|
||||||
}
|
}
|
||||||
} else if (mutedFilterV1 != null) {
|
} else if (mutedFilterV1 != null) {
|
||||||
mutedFilterV1?.let { filter ->
|
mutedFilterV1?.let { filter ->
|
||||||
if (filter.context.size > 1) {
|
if (filter.contexts.size > 1) {
|
||||||
// This filter exists in multiple contexts, just remove the home context
|
// This filter exists in multiple contexts, just remove the home context
|
||||||
mastodonApi.updateFilterV1(
|
mastodonApi.updateFilterV1(
|
||||||
id = filter.id,
|
id = filter.id,
|
||||||
phrase = filter.phrase,
|
phrase = filter.phrase,
|
||||||
context = filter.context.filter { it != FilterV1.HOME },
|
context = filter.contexts.filter { it != FilterContext.HOME },
|
||||||
irreversible = null,
|
irreversible = null,
|
||||||
wholeWord = null,
|
wholeWord = null,
|
||||||
expiresInSeconds = null,
|
expiresInSeconds = null,
|
||||||
@ -373,7 +374,7 @@ class StatusListActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButton
|
|||||||
{
|
{
|
||||||
updateTagMuteState(false)
|
updateTagMuteState(false)
|
||||||
Snackbar.make(binding.root, getString(R.string.confirmation_hashtag_unmuted, hashtag), Snackbar.LENGTH_SHORT).show()
|
Snackbar.make(binding.root, getString(R.string.confirmation_hashtag_unmuted, hashtag), Snackbar.LENGTH_SHORT).show()
|
||||||
eventHub.dispatch(FilterChangedEvent(Filter.Kind.HOME))
|
eventHub.dispatch(FilterChangedEvent(FilterContext.HOME))
|
||||||
mutedFilterV1 = null
|
mutedFilterV1 = null
|
||||||
mutedFilter = null
|
mutedFilter = null
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,7 @@ package app.pachli.appstore
|
|||||||
|
|
||||||
import app.pachli.core.database.model.TabData
|
import app.pachli.core.database.model.TabData
|
||||||
import app.pachli.core.network.model.Account
|
import app.pachli.core.network.model.Account
|
||||||
import app.pachli.core.network.model.Filter
|
import app.pachli.core.network.model.FilterContext
|
||||||
import app.pachli.core.network.model.Poll
|
import app.pachli.core.network.model.Poll
|
||||||
import app.pachli.core.network.model.Status
|
import app.pachli.core.network.model.Status
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ data class StatusComposedEvent(val status: Status) : Event
|
|||||||
data object StatusScheduledEvent : Event
|
data object StatusScheduledEvent : Event
|
||||||
data class StatusEditedEvent(val originalId: String, val status: Status) : Event
|
data class StatusEditedEvent(val originalId: String, val status: Status) : Event
|
||||||
data class ProfileEditedEvent(val newProfileData: Account) : Event
|
data class ProfileEditedEvent(val newProfileData: Account) : Event
|
||||||
data class FilterChangedEvent(val filterKind: Filter.Kind) : Event
|
data class FilterChangedEvent(val filterContext: FilterContext) : Event
|
||||||
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Event
|
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Event
|
||||||
data class PollVoteEvent(val statusId: String, val poll: Poll) : Event
|
data class PollVoteEvent(val statusId: String, val poll: Poll) : Event
|
||||||
data class DomainMuteEvent(val instance: String) : Event
|
data class DomainMuteEvent(val instance: String) : Event
|
||||||
|
@ -18,6 +18,7 @@ import app.pachli.core.common.extensions.viewBinding
|
|||||||
import app.pachli.core.common.extensions.visible
|
import app.pachli.core.common.extensions.visible
|
||||||
import app.pachli.core.navigation.EditFilterActivityIntent
|
import app.pachli.core.navigation.EditFilterActivityIntent
|
||||||
import app.pachli.core.network.model.Filter
|
import app.pachli.core.network.model.Filter
|
||||||
|
import app.pachli.core.network.model.FilterContext
|
||||||
import app.pachli.core.network.model.FilterKeyword
|
import app.pachli.core.network.model.FilterKeyword
|
||||||
import app.pachli.core.network.retrofit.MastodonApi
|
import app.pachli.core.network.retrofit.MastodonApi
|
||||||
import app.pachli.databinding.ActivityEditFilterBinding
|
import app.pachli.databinding.ActivityEditFilterBinding
|
||||||
@ -48,20 +49,20 @@ class EditFilterActivity : BaseActivity() {
|
|||||||
|
|
||||||
private lateinit var filter: Filter
|
private lateinit var filter: Filter
|
||||||
private var originalFilter: Filter? = null
|
private var originalFilter: Filter? = null
|
||||||
private lateinit var contextSwitches: Map<SwitchMaterial, Filter.Kind>
|
private lateinit var filterContextSwitches: Map<SwitchMaterial, FilterContext>
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
originalFilter = EditFilterActivityIntent.getFilter(intent)
|
originalFilter = EditFilterActivityIntent.getFilter(intent)
|
||||||
filter = originalFilter ?: Filter("", "", listOf(), null, Filter.Action.WARN.action, listOf())
|
filter = originalFilter ?: Filter("", "", listOf(), null, Filter.Action.WARN, listOf())
|
||||||
binding.apply {
|
binding.apply {
|
||||||
contextSwitches = mapOf(
|
filterContextSwitches = mapOf(
|
||||||
filterContextHome to Filter.Kind.HOME,
|
filterContextHome to FilterContext.HOME,
|
||||||
filterContextNotifications to Filter.Kind.NOTIFICATIONS,
|
filterContextNotifications to FilterContext.NOTIFICATIONS,
|
||||||
filterContextPublic to Filter.Kind.PUBLIC,
|
filterContextPublic to FilterContext.PUBLIC,
|
||||||
filterContextThread to Filter.Kind.THREAD,
|
filterContextThread to FilterContext.THREAD,
|
||||||
filterContextAccount to Filter.Kind.ACCOUNT,
|
filterContextAccount to FilterContext.ACCOUNT,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,9 +91,9 @@ class EditFilterActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
binding.filterDeleteButton.visible(originalFilter != null)
|
binding.filterDeleteButton.visible(originalFilter != null)
|
||||||
|
|
||||||
for (switch in contextSwitches.keys) {
|
for (switch in filterContextSwitches.keys) {
|
||||||
switch.setOnCheckedChangeListener { _, isChecked ->
|
switch.setOnCheckedChangeListener { _, isChecked ->
|
||||||
val context = contextSwitches[switch]!!
|
val context = filterContextSwitches[switch]!!
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
viewModel.addContext(context)
|
viewModel.addContext(context)
|
||||||
} else {
|
} else {
|
||||||
@ -156,7 +157,7 @@ class EditFilterActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
viewModel.contexts.collect { contexts ->
|
viewModel.contexts.collect { contexts ->
|
||||||
for ((key, value) in contextSwitches) {
|
for ((key, value) in filterContextSwitches) {
|
||||||
key.isChecked = contexts.contains(value)
|
key.isChecked = contexts.contains(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import app.pachli.appstore.EventHub
|
import app.pachli.appstore.EventHub
|
||||||
import app.pachli.appstore.FilterChangedEvent
|
import app.pachli.appstore.FilterChangedEvent
|
||||||
import app.pachli.core.network.model.Filter
|
import app.pachli.core.network.model.Filter
|
||||||
|
import app.pachli.core.network.model.FilterContext
|
||||||
import app.pachli.core.network.model.FilterKeyword
|
import app.pachli.core.network.model.FilterKeyword
|
||||||
import app.pachli.core.network.retrofit.MastodonApi
|
import app.pachli.core.network.retrofit.MastodonApi
|
||||||
import at.connyduck.calladapter.networkresult.fold
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
@ -22,7 +23,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||||||
val keywords = MutableStateFlow(listOf<FilterKeyword>())
|
val keywords = MutableStateFlow(listOf<FilterKeyword>())
|
||||||
val action = MutableStateFlow(Filter.Action.WARN)
|
val action = MutableStateFlow(Filter.Action.WARN)
|
||||||
val duration = MutableStateFlow(0)
|
val duration = MutableStateFlow(0)
|
||||||
val contexts = MutableStateFlow(listOf<Filter.Kind>())
|
val contexts = MutableStateFlow(listOf<FilterContext>())
|
||||||
|
|
||||||
fun load(filter: Filter) {
|
fun load(filter: Filter) {
|
||||||
originalFilter = filter
|
originalFilter = filter
|
||||||
@ -34,7 +35,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||||||
} else {
|
} else {
|
||||||
-1
|
-1
|
||||||
}
|
}
|
||||||
contexts.value = filter.kinds
|
contexts.value = filter.contexts
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addKeyword(keyword: FilterKeyword) {
|
fun addKeyword(keyword: FilterKeyword) {
|
||||||
@ -66,14 +67,14 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||||||
this.action.value = action
|
this.action.value = action
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addContext(context: Filter.Kind) {
|
fun addContext(filterContext: FilterContext) {
|
||||||
if (!contexts.value.contains(context)) {
|
if (!contexts.value.contains(filterContext)) {
|
||||||
contexts.value += context
|
contexts.value += filterContext
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeContext(context: Filter.Kind) {
|
fun removeContext(filterContext: FilterContext) {
|
||||||
contexts.value = contexts.value.filter { it != context }
|
contexts.value = contexts.value.filter { it != filterContext }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validate(): Boolean {
|
fun validate(): Boolean {
|
||||||
@ -83,7 +84,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun saveChanges(context: Context): Boolean {
|
suspend fun saveChanges(context: Context): Boolean {
|
||||||
val contexts = contexts.value.map { it.kind }
|
val contexts = contexts.value
|
||||||
val title = title.value
|
val title = title.value
|
||||||
val durationIndex = duration.value
|
val durationIndex = duration.value
|
||||||
val action = action.value.action
|
val action = action.value.action
|
||||||
@ -97,9 +98,9 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||||||
// e.g., removing a filter from "home" still notifies anything showing
|
// e.g., removing a filter from "home" still notifies anything showing
|
||||||
// the home timeline, so the timeline can be refreshed.
|
// the home timeline, so the timeline can be refreshed.
|
||||||
if (success) {
|
if (success) {
|
||||||
val originalKinds = originalFilter?.context?.map { Filter.Kind.from(it) } ?: emptyList()
|
val originalContexts = originalFilter?.contexts ?: emptyList()
|
||||||
val newKinds = contexts.map { Filter.Kind.from(it) }
|
val newFilterContexts = contexts
|
||||||
(originalKinds + newKinds).distinct().forEach {
|
(originalContexts + newFilterContexts).distinct().forEach {
|
||||||
eventHub.dispatch(FilterChangedEvent(it))
|
eventHub.dispatch(FilterChangedEvent(it))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,7 +108,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createFilter(title: String, contexts: List<String>, action: String, durationIndex: Int, context: Context): Boolean {
|
private suspend fun createFilter(title: String, contexts: List<FilterContext>, action: String, durationIndex: Int, context: Context): Boolean {
|
||||||
val expiresInSeconds = EditFilterActivity.getSecondsForDurationIndex(durationIndex, context)
|
val expiresInSeconds = EditFilterActivity.getSecondsForDurationIndex(durationIndex, context)
|
||||||
api.createFilter(
|
api.createFilter(
|
||||||
title = title,
|
title = title,
|
||||||
@ -131,7 +132,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateFilter(originalFilter: Filter, title: String, contexts: List<String>, action: String, durationIndex: Int, context: Context): Boolean {
|
private suspend fun updateFilter(originalFilter: Filter, title: String, contexts: List<FilterContext>, action: String, durationIndex: Int, context: Context): Boolean {
|
||||||
val expiresInSeconds = EditFilterActivity.getSecondsForDurationIndex(durationIndex, context)
|
val expiresInSeconds = EditFilterActivity.getSecondsForDurationIndex(durationIndex, context)
|
||||||
api.updateFilter(
|
api.updateFilter(
|
||||||
id = originalFilter.id,
|
id = originalFilter.id,
|
||||||
@ -167,18 +168,18 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createFilterV1(context: List<String>, expiresInSeconds: Int?): Boolean {
|
private suspend fun createFilterV1(contexts: List<FilterContext>, expiresInSeconds: Int?): Boolean {
|
||||||
return keywords.value.map { keyword ->
|
return keywords.value.map { keyword ->
|
||||||
api.createFilterV1(keyword.keyword, context, false, keyword.wholeWord, expiresInSeconds)
|
api.createFilterV1(keyword.keyword, contexts, false, keyword.wholeWord, expiresInSeconds)
|
||||||
}.none { it.isFailure }
|
}.none { it.isFailure }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateFilterV1(context: List<String>, expiresInSeconds: Int?): Boolean {
|
private suspend fun updateFilterV1(contexts: List<FilterContext>, expiresInSeconds: Int?): Boolean {
|
||||||
val results = keywords.value.map { keyword ->
|
val results = keywords.value.map { keyword ->
|
||||||
if (originalFilter == null) {
|
if (originalFilter == null) {
|
||||||
api.createFilterV1(
|
api.createFilterV1(
|
||||||
phrase = keyword.keyword,
|
phrase = keyword.keyword,
|
||||||
context = context,
|
context = contexts,
|
||||||
irreversible = false,
|
irreversible = false,
|
||||||
wholeWord = keyword.wholeWord,
|
wholeWord = keyword.wholeWord,
|
||||||
expiresInSeconds = expiresInSeconds,
|
expiresInSeconds = expiresInSeconds,
|
||||||
@ -187,7 +188,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||||||
api.updateFilterV1(
|
api.updateFilterV1(
|
||||||
id = originalFilter!!.id,
|
id = originalFilter!!.id,
|
||||||
phrase = keyword.keyword,
|
phrase = keyword.keyword,
|
||||||
context = context,
|
context = contexts,
|
||||||
irreversible = false,
|
irreversible = false,
|
||||||
wholeWord = keyword.wholeWord,
|
wholeWord = keyword.wholeWord,
|
||||||
expiresInSeconds = expiresInSeconds,
|
expiresInSeconds = expiresInSeconds,
|
||||||
|
@ -22,7 +22,7 @@ class FiltersAdapter(val listener: FiltersListener, val filters: List<Filter>) :
|
|||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
val resources = binding.root.resources
|
val resources = binding.root.resources
|
||||||
val actions = resources.getStringArray(R.array.filter_actions)
|
val actions = resources.getStringArray(R.array.filter_actions)
|
||||||
val contexts = resources.getStringArray(R.array.filter_contexts)
|
val filterContextNames = resources.getStringArray(R.array.filter_contexts)
|
||||||
|
|
||||||
val filter = filters[position]
|
val filter = filters[position]
|
||||||
val context = binding.root.context
|
val context = binding.root.context
|
||||||
@ -37,7 +37,7 @@ class FiltersAdapter(val listener: FiltersListener, val filters: List<Filter>) :
|
|||||||
binding.textSecondary.text = context.getString(
|
binding.textSecondary.text = context.getString(
|
||||||
R.string.filter_description_format,
|
R.string.filter_description_format,
|
||||||
actions.getOrNull(filter.action.ordinal - 1),
|
actions.getOrNull(filter.action.ordinal - 1),
|
||||||
filter.context.map { contexts.getOrNull(Filter.Kind.from(it).ordinal) }.joinToString("/"),
|
filter.contexts.map { filterContextNames.getOrNull(it.ordinal) }.joinToString("/"),
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.delete.setOnClickListener {
|
binding.delete.setOnClickListener {
|
||||||
|
@ -70,8 +70,8 @@ class FiltersViewModel @Inject constructor(
|
|||||||
api.deleteFilter(filter.id).fold(
|
api.deleteFilter(filter.id).fold(
|
||||||
{
|
{
|
||||||
this@FiltersViewModel._state.value = State(this@FiltersViewModel._state.value.filters.filter { it.id != filter.id }, LoadingState.LOADED)
|
this@FiltersViewModel._state.value = State(this@FiltersViewModel._state.value.filters.filter { it.id != filter.id }, LoadingState.LOADED)
|
||||||
for (context in filter.context) {
|
for (context in filter.contexts) {
|
||||||
eventHub.dispatch(FilterChangedEvent(Filter.Kind.from(context)))
|
eventHub.dispatch(FilterChangedEvent(context))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ throwable ->
|
{ throwable ->
|
||||||
@ -79,8 +79,8 @@ class FiltersViewModel @Inject constructor(
|
|||||||
api.deleteFilterV1(filter.id).fold(
|
api.deleteFilterV1(filter.id).fold(
|
||||||
{
|
{
|
||||||
this@FiltersViewModel._state.value = State(this@FiltersViewModel._state.value.filters.filter { it.id != filter.id }, LoadingState.LOADED)
|
this@FiltersViewModel._state.value = State(this@FiltersViewModel._state.value.filters.filter { it.id != filter.id }, LoadingState.LOADED)
|
||||||
filter.context.forEach {
|
filter.contexts.forEach {
|
||||||
eventHub.dispatch(FilterChangedEvent(Filter.Kind.from(it)))
|
eventHub.dispatch(FilterChangedEvent(it))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -35,6 +35,7 @@ import app.pachli.components.timeline.FiltersRepository
|
|||||||
import app.pachli.components.timeline.util.ifExpected
|
import app.pachli.components.timeline.util.ifExpected
|
||||||
import app.pachli.core.accounts.AccountManager
|
import app.pachli.core.accounts.AccountManager
|
||||||
import app.pachli.core.network.model.Filter
|
import app.pachli.core.network.model.Filter
|
||||||
|
import app.pachli.core.network.model.FilterContext
|
||||||
import app.pachli.core.network.model.Notification
|
import app.pachli.core.network.model.Notification
|
||||||
import app.pachli.core.network.model.Poll
|
import app.pachli.core.network.model.Poll
|
||||||
import app.pachli.core.preferences.PrefKeys
|
import app.pachli.core.preferences.PrefKeys
|
||||||
@ -472,7 +473,7 @@ class NotificationsViewModel @Inject constructor(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
eventHub.events
|
eventHub.events
|
||||||
.filterIsInstance<FilterChangedEvent>()
|
.filterIsInstance<FilterChangedEvent>()
|
||||||
.filter { it.filterKind == Filter.Kind.NOTIFICATIONS }
|
.filter { it.filterContext == FilterContext.NOTIFICATIONS }
|
||||||
.map {
|
.map {
|
||||||
getFilters()
|
getFilters()
|
||||||
repository.invalidate()
|
repository.invalidate()
|
||||||
@ -538,8 +539,8 @@ class NotificationsViewModel @Inject constructor(
|
|||||||
private fun getFilters() = viewModelScope.launch {
|
private fun getFilters() = viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
filterModel = when (val filters = filtersRepository.getFilters()) {
|
filterModel = when (val filters = filtersRepository.getFilters()) {
|
||||||
is FilterKind.V1 -> FilterModel(Filter.Kind.NOTIFICATIONS, filters.filters)
|
is FilterKind.V1 -> FilterModel(FilterContext.NOTIFICATIONS, filters.filters)
|
||||||
is FilterKind.V2 -> FilterModel(Filter.Kind.NOTIFICATIONS)
|
is FilterKind.V2 -> FilterModel(FilterContext.NOTIFICATIONS)
|
||||||
}
|
}
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
_uiErrorChannel.send(UiError.GetFilters(throwable))
|
_uiErrorChannel.send(UiError.GetFilters(throwable))
|
||||||
|
@ -46,6 +46,7 @@ import app.pachli.components.timeline.FiltersRepository
|
|||||||
import app.pachli.components.timeline.util.ifExpected
|
import app.pachli.components.timeline.util.ifExpected
|
||||||
import app.pachli.core.accounts.AccountManager
|
import app.pachli.core.accounts.AccountManager
|
||||||
import app.pachli.core.network.model.Filter
|
import app.pachli.core.network.model.Filter
|
||||||
|
import app.pachli.core.network.model.FilterContext
|
||||||
import app.pachli.core.network.model.Poll
|
import app.pachli.core.network.model.Poll
|
||||||
import app.pachli.core.network.model.Status
|
import app.pachli.core.network.model.Status
|
||||||
import app.pachli.core.network.model.TimelineKind
|
import app.pachli.core.network.model.TimelineKind
|
||||||
@ -521,7 +522,7 @@ abstract class TimelineViewModel(
|
|||||||
/** Updates the current set of filters if filter-related preferences change */
|
/** Updates the current set of filters if filter-related preferences change */
|
||||||
private fun updateFiltersFromPreferences() = eventHub.events
|
private fun updateFiltersFromPreferences() = eventHub.events
|
||||||
.filterIsInstance<FilterChangedEvent>()
|
.filterIsInstance<FilterChangedEvent>()
|
||||||
.filter { filterContextMatchesKind(timelineKind, listOf(it.filterKind)) }
|
.filter { filterContextMatchesKind(timelineKind, listOf(it.filterContext)) }
|
||||||
.map {
|
.map {
|
||||||
getFilters()
|
getFilters()
|
||||||
Timber.d("Reload because FilterChangedEvent")
|
Timber.d("Reload because FilterChangedEvent")
|
||||||
@ -534,10 +535,10 @@ abstract class TimelineViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
Timber.d("getFilters()")
|
Timber.d("getFilters()")
|
||||||
try {
|
try {
|
||||||
val filterKind = Filter.Kind.from(timelineKind)
|
val filterContext = FilterContext.from(timelineKind)
|
||||||
filterModel = when (val filters = filtersRepository.getFilters()) {
|
filterModel = when (val filters = filtersRepository.getFilters()) {
|
||||||
is FilterKind.V1 -> FilterModel(filterKind, filters.filters)
|
is FilterKind.V1 -> FilterModel(filterContext, filters.filters)
|
||||||
is FilterKind.V2 -> FilterModel(filterKind)
|
is FilterKind.V2 -> FilterModel(filterContext)
|
||||||
}
|
}
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
Timber.d(throwable, "updateFilter(): Error fetching filters")
|
Timber.d(throwable, "updateFilter(): Error fetching filters")
|
||||||
@ -635,9 +636,9 @@ abstract class TimelineViewModel(
|
|||||||
|
|
||||||
fun filterContextMatchesKind(
|
fun filterContextMatchesKind(
|
||||||
timelineKind: TimelineKind,
|
timelineKind: TimelineKind,
|
||||||
filterContext: List<Filter.Kind>,
|
filterContext: List<FilterContext>,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return filterContext.contains(Filter.Kind.from(timelineKind))
|
return filterContext.contains(FilterContext.from(timelineKind))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.pachli.appstore.EventHub
|
import app.pachli.appstore.EventHub
|
||||||
import app.pachli.appstore.FilterChangedEvent
|
import app.pachli.appstore.FilterChangedEvent
|
||||||
import app.pachli.core.network.model.Filter
|
import app.pachli.core.network.model.FilterContext
|
||||||
import app.pachli.core.network.model.TrendingTag
|
import app.pachli.core.network.model.TrendingTag
|
||||||
import app.pachli.core.network.model.end
|
import app.pachli.core.network.model.end
|
||||||
import app.pachli.core.network.model.start
|
import app.pachli.core.network.model.start
|
||||||
@ -96,7 +96,7 @@ class TrendingTagsViewModel @Inject constructor(
|
|||||||
TrendingTagsUiState(emptyList(), LoadingState.LOADED)
|
TrendingTagsUiState(emptyList(), LoadingState.LOADED)
|
||||||
} else {
|
} else {
|
||||||
val homeFilters = deferredFilters.await().getOrNull()?.filter { filter ->
|
val homeFilters = deferredFilters.await().getOrNull()?.filter { filter ->
|
||||||
filter.context.contains(Filter.Kind.HOME.kind)
|
filter.contexts.contains(FilterContext.HOME)
|
||||||
}
|
}
|
||||||
val tags = tagResponse
|
val tags = tagResponse
|
||||||
.filter { tag ->
|
.filter { tag ->
|
||||||
|
@ -38,6 +38,7 @@ import app.pachli.core.database.model.AccountEntity
|
|||||||
import app.pachli.core.database.model.TranslatedStatusEntity
|
import app.pachli.core.database.model.TranslatedStatusEntity
|
||||||
import app.pachli.core.database.model.TranslationState
|
import app.pachli.core.database.model.TranslationState
|
||||||
import app.pachli.core.network.model.Filter
|
import app.pachli.core.network.model.Filter
|
||||||
|
import app.pachli.core.network.model.FilterContext
|
||||||
import app.pachli.core.network.model.Poll
|
import app.pachli.core.network.model.Poll
|
||||||
import app.pachli.core.network.model.Status
|
import app.pachli.core.network.model.Status
|
||||||
import app.pachli.core.network.retrofit.MastodonApi
|
import app.pachli.core.network.retrofit.MastodonApi
|
||||||
@ -112,7 +113,7 @@ class ViewThreadViewModel @Inject constructor(
|
|||||||
is StatusDeletedEvent -> handleStatusDeletedEvent(event)
|
is StatusDeletedEvent -> handleStatusDeletedEvent(event)
|
||||||
is StatusEditedEvent -> handleStatusEditedEvent(event)
|
is StatusEditedEvent -> handleStatusEditedEvent(event)
|
||||||
is FilterChangedEvent -> {
|
is FilterChangedEvent -> {
|
||||||
if (event.filterKind == Filter.Kind.THREAD) {
|
if (event.filterContext == FilterContext.THREAD) {
|
||||||
loadFilters()
|
loadFilters()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -527,8 +528,8 @@ class ViewThreadViewModel @Inject constructor(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
filterModel = when (val filters = filtersRepository.getFilters()) {
|
filterModel = when (val filters = filtersRepository.getFilters()) {
|
||||||
is FilterKind.V1 -> FilterModel(Filter.Kind.THREAD, filters.filters)
|
is FilterKind.V1 -> FilterModel(FilterContext.THREAD, filters.filters)
|
||||||
is FilterKind.V2 -> FilterModel(Filter.Kind.THREAD)
|
is FilterKind.V2 -> FilterModel(FilterContext.THREAD)
|
||||||
}
|
}
|
||||||
updateStatuses()
|
updateStatuses()
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package app.pachli.network
|
package app.pachli.network
|
||||||
|
|
||||||
import app.pachli.core.network.model.Filter
|
import app.pachli.core.network.model.Filter
|
||||||
|
import app.pachli.core.network.model.FilterContext
|
||||||
import app.pachli.core.network.model.FilterV1
|
import app.pachli.core.network.model.FilterV1
|
||||||
import app.pachli.core.network.model.Status
|
import app.pachli.core.network.model.Status
|
||||||
import app.pachli.core.network.parseAsMastodonHtml
|
import app.pachli.core.network.parseAsMastodonHtml
|
||||||
@ -10,16 +11,16 @@ import java.util.regex.Pattern
|
|||||||
/**
|
/**
|
||||||
* Filter statuses using V1 or V2 filters.
|
* Filter statuses using V1 or V2 filters.
|
||||||
*
|
*
|
||||||
* Construct with [filterKind] that corresponds to the kind of timeline, and optionally the set
|
* Construct with [filterContext] that corresponds to the kind of timeline, and optionally the set
|
||||||
* of v1 filters that should be applied.
|
* of v1 filters that should be applied.
|
||||||
*/
|
*/
|
||||||
class FilterModel constructor(private val filterKind: Filter.Kind, v1filters: List<FilterV1>? = null) {
|
class FilterModel(private val filterContext: FilterContext, v1filters: List<FilterV1>? = null) {
|
||||||
/** Pattern to use when matching v1 filters against a status. Null if these are v2 filters */
|
/** Pattern to use when matching v1 filters against a status. Null if these are v2 filters */
|
||||||
private var pattern: Pattern? = null
|
private var pattern: Pattern? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
pattern = v1filters?.let { list ->
|
pattern = v1filters?.let { list ->
|
||||||
makeFilter(list.filter { it.context.contains(filterKind.kind) })
|
makeFilter(list.filter { it.contexts.contains(filterContext) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +49,7 @@ class FilterModel constructor(private val filterKind: Filter.Kind, v1filters: Li
|
|||||||
}
|
}
|
||||||
|
|
||||||
val matchingKind = status.filtered?.filter { result ->
|
val matchingKind = status.filtered?.filter { result ->
|
||||||
result.filter.kinds.contains(filterKind)
|
result.filter.contexts.contains(filterContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (matchingKind.isNullOrEmpty()) {
|
return if (matchingKind.isNullOrEmpty()) {
|
||||||
|
@ -21,6 +21,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||||||
import app.pachli.components.filters.EditFilterActivity
|
import app.pachli.components.filters.EditFilterActivity
|
||||||
import app.pachli.core.network.model.Attachment
|
import app.pachli.core.network.model.Attachment
|
||||||
import app.pachli.core.network.model.Filter
|
import app.pachli.core.network.model.Filter
|
||||||
|
import app.pachli.core.network.model.FilterContext
|
||||||
import app.pachli.core.network.model.FilterV1
|
import app.pachli.core.network.model.FilterV1
|
||||||
import app.pachli.core.network.model.Poll
|
import app.pachli.core.network.model.Poll
|
||||||
import app.pachli.core.network.model.PollOption
|
import app.pachli.core.network.model.PollOption
|
||||||
@ -45,7 +46,7 @@ class FilterV1Test {
|
|||||||
FilterV1(
|
FilterV1(
|
||||||
id = "123",
|
id = "123",
|
||||||
phrase = "badWord",
|
phrase = "badWord",
|
||||||
context = listOf(FilterV1.HOME),
|
contexts = listOf(FilterContext.HOME),
|
||||||
expiresAt = null,
|
expiresAt = null,
|
||||||
irreversible = false,
|
irreversible = false,
|
||||||
wholeWord = false,
|
wholeWord = false,
|
||||||
@ -53,7 +54,7 @@ class FilterV1Test {
|
|||||||
FilterV1(
|
FilterV1(
|
||||||
id = "123",
|
id = "123",
|
||||||
phrase = "badWholeWord",
|
phrase = "badWholeWord",
|
||||||
context = listOf(FilterV1.HOME, FilterV1.PUBLIC),
|
contexts = listOf(FilterContext.HOME, FilterContext.PUBLIC),
|
||||||
expiresAt = null,
|
expiresAt = null,
|
||||||
irreversible = false,
|
irreversible = false,
|
||||||
wholeWord = true,
|
wholeWord = true,
|
||||||
@ -61,7 +62,7 @@ class FilterV1Test {
|
|||||||
FilterV1(
|
FilterV1(
|
||||||
id = "123",
|
id = "123",
|
||||||
phrase = "@twitter.com",
|
phrase = "@twitter.com",
|
||||||
context = listOf(FilterV1.HOME),
|
contexts = listOf(FilterContext.HOME),
|
||||||
expiresAt = null,
|
expiresAt = null,
|
||||||
irreversible = false,
|
irreversible = false,
|
||||||
wholeWord = true,
|
wholeWord = true,
|
||||||
@ -69,7 +70,7 @@ class FilterV1Test {
|
|||||||
FilterV1(
|
FilterV1(
|
||||||
id = "123",
|
id = "123",
|
||||||
phrase = "#hashtag",
|
phrase = "#hashtag",
|
||||||
context = listOf(FilterV1.HOME),
|
contexts = listOf(FilterContext.HOME),
|
||||||
expiresAt = null,
|
expiresAt = null,
|
||||||
irreversible = false,
|
irreversible = false,
|
||||||
wholeWord = true,
|
wholeWord = true,
|
||||||
@ -77,7 +78,7 @@ class FilterV1Test {
|
|||||||
FilterV1(
|
FilterV1(
|
||||||
id = "123",
|
id = "123",
|
||||||
phrase = "expired",
|
phrase = "expired",
|
||||||
context = listOf(FilterV1.HOME),
|
contexts = listOf(FilterContext.HOME),
|
||||||
expiresAt = Date.from(Instant.now().minusSeconds(10)),
|
expiresAt = Date.from(Instant.now().minusSeconds(10)),
|
||||||
irreversible = false,
|
irreversible = false,
|
||||||
wholeWord = true,
|
wholeWord = true,
|
||||||
@ -85,7 +86,7 @@ class FilterV1Test {
|
|||||||
FilterV1(
|
FilterV1(
|
||||||
id = "123",
|
id = "123",
|
||||||
phrase = "unexpired",
|
phrase = "unexpired",
|
||||||
context = listOf(FilterV1.HOME),
|
contexts = listOf(FilterContext.HOME),
|
||||||
expiresAt = Date.from(Instant.now().plusSeconds(3600)),
|
expiresAt = Date.from(Instant.now().plusSeconds(3600)),
|
||||||
irreversible = false,
|
irreversible = false,
|
||||||
wholeWord = true,
|
wholeWord = true,
|
||||||
@ -93,14 +94,14 @@ class FilterV1Test {
|
|||||||
FilterV1(
|
FilterV1(
|
||||||
id = "123",
|
id = "123",
|
||||||
phrase = "href",
|
phrase = "href",
|
||||||
context = listOf(FilterV1.HOME),
|
contexts = listOf(FilterContext.HOME),
|
||||||
expiresAt = null,
|
expiresAt = null,
|
||||||
irreversible = false,
|
irreversible = false,
|
||||||
wholeWord = false,
|
wholeWord = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
filterModel = FilterModel(Filter.Kind.HOME, filters)
|
filterModel = FilterModel(FilterContext.HOME, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -24,6 +24,7 @@ import app.pachli.core.mastodon.model.MediaUploadApi
|
|||||||
import app.pachli.core.network.BuildConfig
|
import app.pachli.core.network.BuildConfig
|
||||||
import app.pachli.core.network.json.BooleanIfNull
|
import app.pachli.core.network.json.BooleanIfNull
|
||||||
import app.pachli.core.network.json.DefaultIfNull
|
import app.pachli.core.network.json.DefaultIfNull
|
||||||
|
import app.pachli.core.network.json.EnumConstantConverterFactory
|
||||||
import app.pachli.core.network.json.Guarded
|
import app.pachli.core.network.json.Guarded
|
||||||
import app.pachli.core.network.json.HasDefault
|
import app.pachli.core.network.json.HasDefault
|
||||||
import app.pachli.core.network.retrofit.InstanceSwitchAuthInterceptor
|
import app.pachli.core.network.retrofit.InstanceSwitchAuthInterceptor
|
||||||
@ -127,6 +128,7 @@ object NetworkModule {
|
|||||||
): Retrofit {
|
): Retrofit {
|
||||||
return Retrofit.Builder().baseUrl("https://" + MastodonApi.PLACEHOLDER_DOMAIN)
|
return Retrofit.Builder().baseUrl("https://" + MastodonApi.PLACEHOLDER_DOMAIN)
|
||||||
.client(httpClient)
|
.client(httpClient)
|
||||||
|
.addConverterFactory(EnumConstantConverterFactory)
|
||||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||||
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
|
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
|
||||||
.build()
|
.build()
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Pachli Association
|
||||||
|
*
|
||||||
|
* This file is a part of Pachli.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Pachli; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package app.pachli.core.network.json
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
import retrofit2.Converter
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrofit [Converter.Factory] that converts enum constants to strings using the
|
||||||
|
* value of any `@Json(name = ...)` annotation on the constant, falling back to
|
||||||
|
* the enum's name if the annotation is not present.
|
||||||
|
*
|
||||||
|
* This ensures that the same string is used for an enum constant's value whether
|
||||||
|
* it is sent/received as JSON or sent as a [retrofit2.http.FormUrlEncoded] value.
|
||||||
|
*
|
||||||
|
* To install in Retrofit call `.addConverterFactory(EnumConstantConverterFactory)`
|
||||||
|
* on the Retrofit builder.
|
||||||
|
*/
|
||||||
|
object EnumConstantConverterFactory : Converter.Factory() {
|
||||||
|
object EnumConstantConverter : Converter<Enum<*>, String> {
|
||||||
|
override fun convert(enum: Enum<*>): String {
|
||||||
|
return try {
|
||||||
|
enum.javaClass.getField(enum.name).getAnnotation(Json::class.java)?.name
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
} ?: enum.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stringConverter(
|
||||||
|
type: Type,
|
||||||
|
annotations: Array<out Annotation>,
|
||||||
|
retrofit: Retrofit,
|
||||||
|
): Converter<Enum<*>, String>? {
|
||||||
|
return if (type is Class<*> && type.isEnum) EnumConstantConverter else null
|
||||||
|
}
|
||||||
|
}
|
@ -13,9 +13,9 @@ import kotlinx.parcelize.Parcelize
|
|||||||
data class Filter(
|
data class Filter(
|
||||||
val id: String,
|
val id: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val context: List<String>,
|
@Json(name = "context") val contexts: List<FilterContext>,
|
||||||
@Json(name = "expires_at") val expiresAt: Date?,
|
@Json(name = "expires_at") val expiresAt: Date?,
|
||||||
@Json(name = "filter_action") val filterAction: String,
|
@Json(name = "filter_action") val action: Action,
|
||||||
// This should not normally be empty. However, Mastodon does not include
|
// This should not normally be empty. However, Mastodon does not include
|
||||||
// this in a status' `filtered.filter` property (it's not null or empty,
|
// this in a status' `filtered.filter` property (it's not null or empty,
|
||||||
// it's missing) which breaks deserialisation. Patch this by ensuring it's
|
// it's missing) which breaks deserialisation. Patch this by ensuring it's
|
||||||
@ -26,48 +26,14 @@ data class Filter(
|
|||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
@HasDefault
|
@HasDefault
|
||||||
enum class Action(val action: String) {
|
enum class Action(val action: String) {
|
||||||
|
@Json(name = "none")
|
||||||
NONE("none"),
|
NONE("none"),
|
||||||
|
|
||||||
|
@Json(name = "warn")
|
||||||
@Default
|
@Default
|
||||||
WARN("warn"),
|
WARN("warn"),
|
||||||
|
|
||||||
|
@Json(name = "hide")
|
||||||
HIDE("hide"),
|
HIDE("hide"),
|
||||||
;
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun from(action: String): Action = entries.firstOrNull { it.action == action } ?: WARN
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@HasDefault
|
|
||||||
enum class Kind(val kind: String) {
|
|
||||||
HOME("home"),
|
|
||||||
NOTIFICATIONS("notifications"),
|
|
||||||
|
|
||||||
@Default
|
|
||||||
PUBLIC("public"),
|
|
||||||
THREAD("thread"),
|
|
||||||
ACCOUNT("account"),
|
|
||||||
;
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun from(kind: String): Kind = entries.firstOrNull { it.kind == kind } ?: PUBLIC
|
|
||||||
|
|
||||||
fun from(kind: TimelineKind): Kind = when (kind) {
|
|
||||||
is TimelineKind.Home, is TimelineKind.UserList -> HOME
|
|
||||||
is TimelineKind.PublicFederated,
|
|
||||||
is TimelineKind.PublicLocal,
|
|
||||||
is TimelineKind.Tag,
|
|
||||||
is TimelineKind.Favourites,
|
|
||||||
-> PUBLIC
|
|
||||||
is TimelineKind.User -> ACCOUNT
|
|
||||||
else -> PUBLIC
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val action: Action
|
|
||||||
get() = Action.from(filterAction)
|
|
||||||
|
|
||||||
val kinds: List<Kind>
|
|
||||||
get() = context.map { Kind.from(it) }
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Pachli Association
|
||||||
|
*
|
||||||
|
* This file is a part of Pachli.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Pachli; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package app.pachli.core.network.model
|
||||||
|
|
||||||
|
import app.pachli.core.network.json.Default
|
||||||
|
import app.pachli.core.network.json.HasDefault
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The contexts in which a filter should be applied, for both a
|
||||||
|
* [v2](https://docs.joinmastodon.org/entities/Filter/#context) and
|
||||||
|
* [v1](https://docs.joinmastodon.org/entities/V1_Filter/#context) Mastodon
|
||||||
|
* filter. The API versions have identical contexts.
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = false)
|
||||||
|
@HasDefault
|
||||||
|
enum class FilterContext {
|
||||||
|
/** Filter applies to home timeline and lists */
|
||||||
|
@Json(name = "home")
|
||||||
|
HOME,
|
||||||
|
|
||||||
|
/** Filter applies to notifications */
|
||||||
|
@Json(name = "notifications")
|
||||||
|
NOTIFICATIONS,
|
||||||
|
|
||||||
|
/** Filter applies to public timelines */
|
||||||
|
@Default
|
||||||
|
@Json(name = "public")
|
||||||
|
PUBLIC,
|
||||||
|
|
||||||
|
/** Filter applies to expanded thread */
|
||||||
|
@Json(name = "thread")
|
||||||
|
THREAD,
|
||||||
|
|
||||||
|
/** Filter applies when viewing a profile */
|
||||||
|
@Json(name = "account")
|
||||||
|
ACCOUNT,
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(kind: TimelineKind): FilterContext = when (kind) {
|
||||||
|
is TimelineKind.Home, is TimelineKind.UserList -> HOME
|
||||||
|
is TimelineKind.PublicFederated,
|
||||||
|
is TimelineKind.PublicLocal,
|
||||||
|
is TimelineKind.Tag,
|
||||||
|
is TimelineKind.Favourites,
|
||||||
|
-> PUBLIC
|
||||||
|
is TimelineKind.User -> ACCOUNT
|
||||||
|
else -> PUBLIC
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -24,19 +24,11 @@ import java.util.Date
|
|||||||
data class FilterV1(
|
data class FilterV1(
|
||||||
val id: String,
|
val id: String,
|
||||||
val phrase: String,
|
val phrase: String,
|
||||||
val context: List<String>,
|
@Json(name = "context") val contexts: List<FilterContext>,
|
||||||
@Json(name = "expires_at") val expiresAt: Date?,
|
@Json(name = "expires_at") val expiresAt: Date?,
|
||||||
val irreversible: Boolean,
|
val irreversible: Boolean,
|
||||||
@Json(name = "whole_word") val wholeWord: Boolean,
|
@Json(name = "whole_word") val wholeWord: Boolean,
|
||||||
) {
|
) {
|
||||||
companion object {
|
|
||||||
const val HOME = "home"
|
|
||||||
const val NOTIFICATIONS = "notifications"
|
|
||||||
const val PUBLIC = "public"
|
|
||||||
const val THREAD = "thread"
|
|
||||||
const val ACCOUNT = "account"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return id.hashCode()
|
return id.hashCode()
|
||||||
}
|
}
|
||||||
@ -53,9 +45,9 @@ data class FilterV1(
|
|||||||
return Filter(
|
return Filter(
|
||||||
id = id,
|
id = id,
|
||||||
title = phrase,
|
title = phrase,
|
||||||
context = context,
|
contexts = contexts,
|
||||||
expiresAt = expiresAt,
|
expiresAt = expiresAt,
|
||||||
filterAction = Filter.Action.WARN.action,
|
action = Filter.Action.WARN,
|
||||||
keywords = listOf(
|
keywords = listOf(
|
||||||
FilterKeyword(
|
FilterKeyword(
|
||||||
id = id,
|
id = id,
|
||||||
|
@ -25,6 +25,7 @@ import app.pachli.core.network.model.Conversation
|
|||||||
import app.pachli.core.network.model.DeletedStatus
|
import app.pachli.core.network.model.DeletedStatus
|
||||||
import app.pachli.core.network.model.Emoji
|
import app.pachli.core.network.model.Emoji
|
||||||
import app.pachli.core.network.model.Filter
|
import app.pachli.core.network.model.Filter
|
||||||
|
import app.pachli.core.network.model.FilterContext
|
||||||
import app.pachli.core.network.model.FilterKeyword
|
import app.pachli.core.network.model.FilterKeyword
|
||||||
import app.pachli.core.network.model.FilterV1
|
import app.pachli.core.network.model.FilterV1
|
||||||
import app.pachli.core.network.model.HashTag
|
import app.pachli.core.network.model.HashTag
|
||||||
@ -614,7 +615,7 @@ interface MastodonApi {
|
|||||||
@POST("api/v1/filters")
|
@POST("api/v1/filters")
|
||||||
suspend fun createFilterV1(
|
suspend fun createFilterV1(
|
||||||
@Field("phrase") phrase: String,
|
@Field("phrase") phrase: String,
|
||||||
@Field("context[]") context: List<String>,
|
@Field("context[]") context: List<FilterContext>,
|
||||||
@Field("irreversible") irreversible: Boolean?,
|
@Field("irreversible") irreversible: Boolean?,
|
||||||
@Field("whole_word") wholeWord: Boolean?,
|
@Field("whole_word") wholeWord: Boolean?,
|
||||||
@Field("expires_in") expiresInSeconds: Int?,
|
@Field("expires_in") expiresInSeconds: Int?,
|
||||||
@ -625,7 +626,7 @@ interface MastodonApi {
|
|||||||
suspend fun updateFilterV1(
|
suspend fun updateFilterV1(
|
||||||
@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<FilterContext>,
|
||||||
@Field("irreversible") irreversible: Boolean?,
|
@Field("irreversible") irreversible: Boolean?,
|
||||||
@Field("whole_word") wholeWord: Boolean?,
|
@Field("whole_word") wholeWord: Boolean?,
|
||||||
@Field("expires_in") expiresInSeconds: Int?,
|
@Field("expires_in") expiresInSeconds: Int?,
|
||||||
@ -640,7 +641,7 @@ interface MastodonApi {
|
|||||||
@POST("api/v2/filters")
|
@POST("api/v2/filters")
|
||||||
suspend fun createFilter(
|
suspend fun createFilter(
|
||||||
@Field("title") title: String,
|
@Field("title") title: String,
|
||||||
@Field("context[]") context: List<String>,
|
@Field("context[]") context: List<FilterContext>,
|
||||||
@Field("filter_action") filterAction: String,
|
@Field("filter_action") filterAction: String,
|
||||||
@Field("expires_in") expiresInSeconds: Int?,
|
@Field("expires_in") expiresInSeconds: Int?,
|
||||||
): NetworkResult<Filter>
|
): NetworkResult<Filter>
|
||||||
@ -650,7 +651,7 @@ interface MastodonApi {
|
|||||||
suspend fun updateFilter(
|
suspend fun updateFilter(
|
||||||
@Path("id") id: String,
|
@Path("id") id: String,
|
||||||
@Field("title") title: String? = null,
|
@Field("title") title: String? = null,
|
||||||
@Field("context[]") context: List<String>? = null,
|
@Field("context[]") context: List<FilterContext>? = null,
|
||||||
@Field("filter_action") filterAction: String? = null,
|
@Field("filter_action") filterAction: String? = null,
|
||||||
@Field("expires_in") expiresInSeconds: Int? = null,
|
@Field("expires_in") expiresInSeconds: Int? = null,
|
||||||
): NetworkResult<Filter>
|
): NetworkResult<Filter>
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
package app.pachli.core.network.json
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class EnumConstantConverterFactoryTest {
|
||||||
|
enum class Enum {
|
||||||
|
@Json(name = "one")
|
||||||
|
ONE,
|
||||||
|
|
||||||
|
TWO,
|
||||||
|
}
|
||||||
|
|
||||||
|
private val converter = EnumConstantConverterFactory.EnumConstantConverter
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Annotated enum constant uses annotation`() {
|
||||||
|
assertThat(converter.convert(Enum.ONE)).isEqualTo("one")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Unannotated enum constant uses constant name`() {
|
||||||
|
assertThat(converter.convert(Enum.TWO)).isEqualTo("TWO")
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user