mirror of
https://github.com/pachli/pachli-android.git
synced 2025-02-03 10:47:34 +01:00
refactor: Create V2 filters with one API call (#979)
Previous code didn't encode v2 filter keywords, so created v2 filters by first creating the filter with no keywords (one API call) then making 1-N API calls to add each keyword to the filter. Fix this by adding a dedicated converter for the `NewContentFilter` type that encodes it correctly so the filter can be created with a single API call. This necessitates moving some types around,
This commit is contained in:
parent
ec27aa2435
commit
65c73625f6
@ -30,16 +30,16 @@ import app.pachli.appstore.MainTabsChangedEvent
|
||||
import app.pachli.core.activity.BottomSheetActivity
|
||||
import app.pachli.core.common.extensions.viewBinding
|
||||
import app.pachli.core.common.util.unsafeLazy
|
||||
import app.pachli.core.data.model.ContentFilter
|
||||
import app.pachli.core.data.model.NewContentFilterKeyword
|
||||
import app.pachli.core.data.repository.ContentFilterEdit
|
||||
import app.pachli.core.data.repository.ContentFiltersError
|
||||
import app.pachli.core.data.repository.ContentFiltersRepository
|
||||
import app.pachli.core.data.repository.NewContentFilter
|
||||
import app.pachli.core.model.ContentFilter
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.model.FilterContext
|
||||
import app.pachli.core.model.NewContentFilter
|
||||
import app.pachli.core.model.NewContentFilterKeyword
|
||||
import app.pachli.core.model.Timeline
|
||||
import app.pachli.core.navigation.TimelineActivityIntent
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.network.model.FilterContext
|
||||
import app.pachli.databinding.ActivityTimelineBinding
|
||||
import app.pachli.interfaces.ActionButtonActivity
|
||||
import app.pachli.interfaces.AppBarLayoutHost
|
||||
|
@ -21,8 +21,9 @@ import android.view.View
|
||||
import androidx.core.text.HtmlCompat
|
||||
import app.pachli.R
|
||||
import app.pachli.core.data.model.StatusDisplayOptions
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.network.model.Filter
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.network.model.FilterAction as NetworkFilterAction
|
||||
import app.pachli.databinding.ItemStatusWrapperBinding
|
||||
import app.pachli.interfaces.StatusActionListener
|
||||
import app.pachli.viewdata.IStatusViewData
|
||||
@ -53,7 +54,7 @@ open class FilterableStatusViewHolder<T : IStatusViewData>(
|
||||
return
|
||||
}
|
||||
|
||||
status.actionable.filtered?.find { it.filter.filterAction === FilterAction.WARN }?.let { result ->
|
||||
status.actionable.filtered?.find { it.filter.filterAction === NetworkFilterAction.WARN }?.let { result ->
|
||||
this.matchedFilter = result.filter
|
||||
setPlaceholderVisibility(true)
|
||||
|
||||
|
@ -27,8 +27,8 @@ import app.pachli.core.common.extensions.visible
|
||||
import app.pachli.core.common.string.unicodeWrap
|
||||
import app.pachli.core.common.util.formatNumber
|
||||
import app.pachli.core.data.model.StatusDisplayOptions
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.network.model.Emoji
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.databinding.ItemStatusBinding
|
||||
import app.pachli.interfaces.StatusActionListener
|
||||
import app.pachli.util.SmartLengthInputFilter
|
||||
|
@ -12,7 +12,7 @@ import app.pachli.core.common.extensions.hide
|
||||
import app.pachli.core.common.extensions.show
|
||||
import app.pachli.core.common.extensions.viewBinding
|
||||
import app.pachli.core.common.extensions.visible
|
||||
import app.pachli.core.data.model.ContentFilter
|
||||
import app.pachli.core.model.ContentFilter
|
||||
import app.pachli.core.navigation.EditContentFilterActivityIntent
|
||||
import app.pachli.core.ui.BackgroundMessage
|
||||
import app.pachli.databinding.ActivityContentFiltersBinding
|
||||
|
@ -1,6 +1,6 @@
|
||||
package app.pachli.components.filters
|
||||
|
||||
import app.pachli.core.data.model.ContentFilter
|
||||
import app.pachli.core.model.ContentFilter
|
||||
|
||||
interface ContentFiltersListener {
|
||||
fun deleteContentFilter(contentFilter: ContentFilter)
|
||||
|
@ -3,8 +3,8 @@ package app.pachli.components.filters
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.pachli.core.data.model.ContentFilter
|
||||
import app.pachli.core.data.repository.ContentFiltersRepository
|
||||
import app.pachli.core.model.ContentFilter
|
||||
import com.github.michaelbull.result.onFailure
|
||||
import com.github.michaelbull.result.onSuccess
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
@ -22,11 +22,10 @@ import app.pachli.R
|
||||
import app.pachli.core.activity.BaseActivity
|
||||
import app.pachli.core.common.extensions.viewBinding
|
||||
import app.pachli.core.common.extensions.visible
|
||||
import app.pachli.core.data.model.ContentFilterValidationError
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.model.FilterContext
|
||||
import app.pachli.core.model.FilterKeyword
|
||||
import app.pachli.core.navigation.EditContentFilterActivityIntent
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.network.model.FilterContext
|
||||
import app.pachli.core.network.model.FilterKeyword
|
||||
import app.pachli.core.ui.extensions.await
|
||||
import app.pachli.databinding.ActivityEditContentFilterBinding
|
||||
import app.pachli.databinding.DialogFilterBinding
|
||||
|
@ -24,15 +24,14 @@ import androidx.lifecycle.viewModelScope
|
||||
import app.pachli.R
|
||||
import app.pachli.core.common.PachliError
|
||||
import app.pachli.core.common.extensions.mapIfNotNull
|
||||
import app.pachli.core.data.model.ContentFilter
|
||||
import app.pachli.core.data.model.ContentFilterValidationError
|
||||
import app.pachli.core.data.model.NewContentFilterKeyword
|
||||
import app.pachli.core.data.repository.ContentFilterEdit
|
||||
import app.pachli.core.data.repository.ContentFiltersRepository
|
||||
import app.pachli.core.data.repository.NewContentFilter
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.network.model.FilterContext
|
||||
import app.pachli.core.network.model.FilterKeyword
|
||||
import app.pachli.core.model.ContentFilter
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.model.FilterContext
|
||||
import app.pachli.core.model.FilterKeyword
|
||||
import app.pachli.core.model.NewContentFilter
|
||||
import app.pachli.core.model.NewContentFilterKeyword
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
|
@ -5,8 +5,7 @@ import android.view.ViewGroup
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import app.pachli.R
|
||||
import app.pachli.core.data.model.ContentFilter
|
||||
import app.pachli.core.data.model.ContentFilterValidationError
|
||||
import app.pachli.core.model.ContentFilter
|
||||
import app.pachli.core.ui.BindingHolder
|
||||
import app.pachli.databinding.ItemRemovableBinding
|
||||
import app.pachli.util.getRelativeTimeSpanString
|
||||
@ -67,6 +66,18 @@ class FiltersAdapter(val listener: ContentFiltersListener, val contentFilters: L
|
||||
}
|
||||
}
|
||||
|
||||
/** Reasons why a filter might be invalid */
|
||||
enum class ContentFilterValidationError {
|
||||
/** Filter title is empty or blank */
|
||||
NO_TITLE,
|
||||
|
||||
/** Filter has no keywords */
|
||||
NO_KEYWORDS,
|
||||
|
||||
/** Filter has no contexts */
|
||||
NO_CONTEXT,
|
||||
}
|
||||
|
||||
/**
|
||||
* @return String resource containing an error message for this
|
||||
* validation error.
|
||||
@ -77,3 +88,13 @@ fun ContentFilterValidationError.stringResource() = when (this) {
|
||||
ContentFilterValidationError.NO_KEYWORDS -> R.string.error_filter_missing_keyword
|
||||
ContentFilterValidationError.NO_CONTEXT -> R.string.error_filter_missing_context
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Set of [ContentFilterValidationError] given the current state of the
|
||||
* filter. Empty if there are no validation errors.
|
||||
*/
|
||||
fun ContentFilter.validate() = buildSet {
|
||||
if (title.isBlank()) add(ContentFilterValidationError.NO_TITLE)
|
||||
if (keywords.isEmpty()) add(ContentFilterValidationError.NO_KEYWORDS)
|
||||
if (contexts.isEmpty()) add(ContentFilterValidationError.NO_CONTEXT)
|
||||
}
|
||||
|
@ -55,9 +55,9 @@ import app.pachli.core.activity.openLink
|
||||
import app.pachli.core.common.extensions.hide
|
||||
import app.pachli.core.common.extensions.show
|
||||
import app.pachli.core.common.extensions.viewBinding
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.navigation.AttachmentViewData.Companion.list
|
||||
import app.pachli.core.navigation.EditContentFilterActivityIntent
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.network.model.Notification
|
||||
import app.pachli.core.network.model.Poll
|
||||
import app.pachli.core.network.model.Status
|
||||
|
@ -26,7 +26,7 @@ import app.pachli.adapter.FollowRequestViewHolder
|
||||
import app.pachli.adapter.ReportNotificationViewHolder
|
||||
import app.pachli.core.common.util.AbsoluteTimeFormatter
|
||||
import app.pachli.core.data.model.StatusDisplayOptions
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.network.model.Notification
|
||||
import app.pachli.core.network.model.Status
|
||||
import app.pachli.databinding.ItemFollowBinding
|
||||
|
@ -32,11 +32,11 @@ import app.pachli.appstore.MuteConversationEvent
|
||||
import app.pachli.appstore.MuteEvent
|
||||
import app.pachli.core.common.extensions.throttleFirst
|
||||
import app.pachli.core.data.repository.AccountManager
|
||||
import app.pachli.core.data.repository.ContentFilterVersion
|
||||
import app.pachli.core.data.repository.ContentFiltersRepository
|
||||
import app.pachli.core.data.repository.StatusDisplayOptionsRepository
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.network.model.FilterContext
|
||||
import app.pachli.core.model.ContentFilterVersion
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.model.FilterContext
|
||||
import app.pachli.core.network.model.Notification
|
||||
import app.pachli.core.network.model.Poll
|
||||
import app.pachli.core.preferences.PrefKeys
|
||||
|
@ -26,7 +26,7 @@ import app.pachli.adapter.FilterableStatusViewHolder
|
||||
import app.pachli.adapter.StatusBaseViewHolder
|
||||
import app.pachli.adapter.StatusViewHolder
|
||||
import app.pachli.core.data.model.StatusDisplayOptions
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.databinding.ItemStatusBinding
|
||||
import app.pachli.databinding.ItemStatusWrapperBinding
|
||||
import app.pachli.interfaces.StatusActionListener
|
||||
|
@ -33,7 +33,7 @@ import app.pachli.components.timeline.CachedTimelineRepository
|
||||
import app.pachli.core.data.repository.AccountManager
|
||||
import app.pachli.core.data.repository.ContentFiltersRepository
|
||||
import app.pachli.core.data.repository.StatusDisplayOptionsRepository
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.network.model.Poll
|
||||
import app.pachli.core.preferences.SharedPreferencesRepository
|
||||
import app.pachli.usecase.TimelineCases
|
||||
|
@ -33,7 +33,7 @@ import app.pachli.components.timeline.NetworkTimelineRepository
|
||||
import app.pachli.core.data.repository.AccountManager
|
||||
import app.pachli.core.data.repository.ContentFiltersRepository
|
||||
import app.pachli.core.data.repository.StatusDisplayOptionsRepository
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.network.model.Poll
|
||||
import app.pachli.core.preferences.SharedPreferencesRepository
|
||||
import app.pachli.usecase.TimelineCases
|
||||
|
@ -43,12 +43,12 @@ import app.pachli.appstore.StatusEditedEvent
|
||||
import app.pachli.appstore.UnfollowEvent
|
||||
import app.pachli.core.common.extensions.throttleFirst
|
||||
import app.pachli.core.data.repository.AccountManager
|
||||
import app.pachli.core.data.repository.ContentFilterVersion
|
||||
import app.pachli.core.data.repository.ContentFiltersRepository
|
||||
import app.pachli.core.data.repository.StatusDisplayOptionsRepository
|
||||
import app.pachli.core.model.ContentFilterVersion
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.model.FilterContext
|
||||
import app.pachli.core.model.Timeline
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.network.model.FilterContext
|
||||
import app.pachli.core.network.model.Poll
|
||||
import app.pachli.core.network.model.Status
|
||||
import app.pachli.core.preferences.PrefKeys
|
||||
|
@ -19,7 +19,7 @@ package app.pachli.components.trending.viewmodel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.pachli.core.data.repository.ContentFiltersRepository
|
||||
import app.pachli.core.network.model.FilterContext
|
||||
import app.pachli.core.model.FilterContext
|
||||
import app.pachli.core.network.model.TrendingTag
|
||||
import app.pachli.core.network.model.end
|
||||
import app.pachli.core.network.model.start
|
||||
|
@ -25,7 +25,7 @@ import app.pachli.adapter.StatusBaseViewHolder
|
||||
import app.pachli.adapter.StatusDetailedViewHolder
|
||||
import app.pachli.adapter.StatusViewHolder
|
||||
import app.pachli.core.data.model.StatusDisplayOptions
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.databinding.ItemStatusBinding
|
||||
import app.pachli.databinding.ItemStatusDetailedBinding
|
||||
import app.pachli.databinding.ItemStatusWrapperBinding
|
||||
|
@ -30,15 +30,15 @@ import app.pachli.appstore.StatusEditedEvent
|
||||
import app.pachli.components.timeline.CachedTimelineRepository
|
||||
import app.pachli.components.timeline.util.ifExpected
|
||||
import app.pachli.core.data.repository.AccountManager
|
||||
import app.pachli.core.data.repository.ContentFilterVersion
|
||||
import app.pachli.core.data.repository.ContentFiltersRepository
|
||||
import app.pachli.core.data.repository.StatusDisplayOptionsRepository
|
||||
import app.pachli.core.database.dao.TimelineDao
|
||||
import app.pachli.core.database.model.AccountEntity
|
||||
import app.pachli.core.database.model.TranslatedStatusEntity
|
||||
import app.pachli.core.database.model.TranslationState
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.network.model.FilterContext
|
||||
import app.pachli.core.model.ContentFilterVersion
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.model.FilterContext
|
||||
import app.pachli.core.network.model.Poll
|
||||
import app.pachli.core.network.model.Status
|
||||
import app.pachli.core.network.retrofit.MastodonApi
|
||||
|
@ -1,8 +1,10 @@
|
||||
package app.pachli.network
|
||||
|
||||
import app.pachli.core.data.model.ContentFilter
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.network.model.FilterContext
|
||||
import app.pachli.core.data.model.from
|
||||
import app.pachli.core.model.ContentFilter
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.model.FilterContext
|
||||
import app.pachli.core.network.model.FilterContext as NetworkFilterContext
|
||||
import app.pachli.core.network.model.Status
|
||||
import app.pachli.core.network.parseAsMastodonHtml
|
||||
import java.util.Date
|
||||
@ -49,13 +51,13 @@ class ContentFilterModel(private val filterContext: FilterContext, v1ContentFilt
|
||||
}
|
||||
|
||||
val matchingKind = status.filtered?.filter { result ->
|
||||
result.filter.contexts.contains(filterContext)
|
||||
result.filter.contexts.contains(NetworkFilterContext.from(filterContext))
|
||||
}
|
||||
|
||||
return if (matchingKind.isNullOrEmpty()) {
|
||||
FilterAction.NONE
|
||||
} else {
|
||||
matchingKind.maxOf { it.filter.filterAction }
|
||||
matchingKind.maxOf { FilterAction.from(it.filter.filterAction) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ package app.pachli.viewdata
|
||||
import android.text.Spanned
|
||||
import app.pachli.core.database.model.TranslatedStatusEntity
|
||||
import app.pachli.core.database.model.TranslationState
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.network.model.Notification
|
||||
import app.pachli.core.network.model.RelationshipSeveranceEvent
|
||||
import app.pachli.core.network.model.Report
|
||||
|
@ -23,7 +23,7 @@ import app.pachli.core.database.model.ConversationStatusEntity
|
||||
import app.pachli.core.database.model.TimelineStatusWithAccount
|
||||
import app.pachli.core.database.model.TranslatedStatusEntity
|
||||
import app.pachli.core.database.model.TranslationState
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.network.model.Status
|
||||
import app.pachli.core.network.parseAsMastodonHtml
|
||||
import app.pachli.core.network.replaceCrashingCharacters
|
||||
|
@ -19,10 +19,12 @@ package app.pachli
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import app.pachli.components.filters.EditContentFilterViewModel.Companion.getSecondsForDurationIndex
|
||||
import app.pachli.core.data.model.ContentFilter
|
||||
import app.pachli.core.data.model.from
|
||||
import app.pachli.core.model.ContentFilter
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.model.FilterContext
|
||||
import app.pachli.core.network.model.Attachment
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.network.model.FilterContext
|
||||
import app.pachli.core.network.model.FilterContext as NetworkFilterContext
|
||||
import app.pachli.core.network.model.FilterV1
|
||||
import app.pachli.core.network.model.Poll
|
||||
import app.pachli.core.network.model.PollOption
|
||||
@ -47,7 +49,7 @@ class ContentFilterV1Test {
|
||||
FilterV1(
|
||||
id = "123",
|
||||
phrase = "badWord",
|
||||
contexts = setOf(FilterContext.HOME),
|
||||
contexts = setOf(NetworkFilterContext.HOME),
|
||||
expiresAt = null,
|
||||
irreversible = false,
|
||||
wholeWord = false,
|
||||
@ -55,7 +57,7 @@ class ContentFilterV1Test {
|
||||
FilterV1(
|
||||
id = "123",
|
||||
phrase = "badWholeWord",
|
||||
contexts = setOf(FilterContext.HOME, FilterContext.PUBLIC),
|
||||
contexts = setOf(NetworkFilterContext.HOME, NetworkFilterContext.PUBLIC),
|
||||
expiresAt = null,
|
||||
irreversible = false,
|
||||
wholeWord = true,
|
||||
@ -63,7 +65,7 @@ class ContentFilterV1Test {
|
||||
FilterV1(
|
||||
id = "123",
|
||||
phrase = "@twitter.com",
|
||||
contexts = setOf(FilterContext.HOME),
|
||||
contexts = setOf(NetworkFilterContext.HOME),
|
||||
expiresAt = null,
|
||||
irreversible = false,
|
||||
wholeWord = true,
|
||||
@ -71,7 +73,7 @@ class ContentFilterV1Test {
|
||||
FilterV1(
|
||||
id = "123",
|
||||
phrase = "#hashtag",
|
||||
contexts = setOf(FilterContext.HOME),
|
||||
contexts = setOf(NetworkFilterContext.HOME),
|
||||
expiresAt = null,
|
||||
irreversible = false,
|
||||
wholeWord = true,
|
||||
@ -79,7 +81,7 @@ class ContentFilterV1Test {
|
||||
FilterV1(
|
||||
id = "123",
|
||||
phrase = "expired",
|
||||
contexts = setOf(FilterContext.HOME),
|
||||
contexts = setOf(NetworkFilterContext.HOME),
|
||||
expiresAt = Date.from(Instant.now().minusSeconds(10)),
|
||||
irreversible = false,
|
||||
wholeWord = true,
|
||||
@ -87,7 +89,7 @@ class ContentFilterV1Test {
|
||||
FilterV1(
|
||||
id = "123",
|
||||
phrase = "unexpired",
|
||||
contexts = setOf(FilterContext.HOME),
|
||||
contexts = setOf(NetworkFilterContext.HOME),
|
||||
expiresAt = Date.from(Instant.now().plusSeconds(3600)),
|
||||
irreversible = false,
|
||||
wholeWord = true,
|
||||
@ -95,7 +97,7 @@ class ContentFilterV1Test {
|
||||
FilterV1(
|
||||
id = "123",
|
||||
phrase = "href",
|
||||
contexts = setOf(FilterContext.HOME),
|
||||
contexts = setOf(NetworkFilterContext.HOME),
|
||||
expiresAt = null,
|
||||
irreversible = false,
|
||||
wholeWord = false,
|
||||
|
@ -17,11 +17,11 @@
|
||||
|
||||
package app.pachli.components.filters
|
||||
|
||||
import app.pachli.core.data.model.ContentFilter
|
||||
import app.pachli.core.data.repository.ContentFilterEdit
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.network.model.FilterContext
|
||||
import app.pachli.core.network.model.FilterKeyword
|
||||
import app.pachli.core.model.ContentFilter
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.model.FilterContext
|
||||
import app.pachli.core.model.FilterKeyword
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
|
||||
|
@ -17,98 +17,78 @@
|
||||
|
||||
package app.pachli.core.data.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.network.model.FilterContext
|
||||
import app.pachli.core.network.model.FilterKeyword
|
||||
import java.util.Date
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/** Reasons why a filter might be invalid */
|
||||
enum class ContentFilterValidationError {
|
||||
/** Filter title is empty or blank */
|
||||
NO_TITLE,
|
||||
|
||||
/** Filter has no keywords */
|
||||
NO_KEYWORDS,
|
||||
|
||||
/** Filter has no contexts */
|
||||
NO_CONTEXT,
|
||||
}
|
||||
import app.pachli.core.model.ContentFilter
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.model.FilterAction.HIDE
|
||||
import app.pachli.core.model.FilterAction.NONE
|
||||
import app.pachli.core.model.FilterAction.WARN
|
||||
import app.pachli.core.model.FilterContext
|
||||
import app.pachli.core.model.FilterContext.ACCOUNT
|
||||
import app.pachli.core.model.FilterContext.HOME
|
||||
import app.pachli.core.model.FilterContext.NOTIFICATIONS
|
||||
import app.pachli.core.model.FilterContext.PUBLIC
|
||||
import app.pachli.core.model.FilterContext.THREAD
|
||||
import app.pachli.core.model.FilterKeyword
|
||||
import app.pachli.core.network.model.Filter as NetworkFilter
|
||||
import app.pachli.core.network.model.FilterAction as NetworkFilterAction
|
||||
import app.pachli.core.network.model.FilterContext as NetworkFilterContext
|
||||
import app.pachli.core.network.model.FilterKeyword as NetworkFilterKeyword
|
||||
import app.pachli.core.network.model.FilterV1 as NetworkFilterV1
|
||||
|
||||
/**
|
||||
* Internal representation of a Mastodon filter, whether v1 or v2.
|
||||
*
|
||||
* This is a *content* filter, to distinguish it from filters that operate on
|
||||
* accounts, domains, or other data.
|
||||
*
|
||||
* @param id The server's ID for this filter
|
||||
* @param title Filter's title (label to use in the UI)
|
||||
* @param contexts One or more [FilterContext] the filter is applied to
|
||||
* @param expiresAt Date the filter expires, null if the filter does not expire
|
||||
* @param filterAction Action to take if the filter matches a status
|
||||
* @param keywords One or more [FilterKeyword] the filter matches against a status
|
||||
* Returns a [ContentFilter] from a [v2 Mastodon filter][NetworkFilter].
|
||||
*/
|
||||
@Parcelize
|
||||
data class ContentFilter(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val contexts: Set<FilterContext> = emptySet(),
|
||||
val expiresAt: Date? = null,
|
||||
val filterAction: FilterAction,
|
||||
val keywords: List<FilterKeyword> = emptyList(),
|
||||
) : Parcelable {
|
||||
/**
|
||||
* @return Set of [ContentFilterValidationError] given the current state of the
|
||||
* filter. Empty if there are no validation errors.
|
||||
*/
|
||||
fun validate() = buildSet {
|
||||
if (title.isBlank()) add(ContentFilterValidationError.NO_TITLE)
|
||||
if (keywords.isEmpty()) add(ContentFilterValidationError.NO_KEYWORDS)
|
||||
if (contexts.isEmpty()) add(ContentFilterValidationError.NO_CONTEXT)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Returns a [ContentFilter] from a
|
||||
* [v2 Mastodon filter][app.pachli.core.network.model.Filter].
|
||||
*/
|
||||
fun from(filter: app.pachli.core.network.model.Filter) = ContentFilter(
|
||||
id = filter.id,
|
||||
title = filter.title,
|
||||
contexts = filter.contexts,
|
||||
expiresAt = filter.expiresAt,
|
||||
filterAction = filter.filterAction,
|
||||
keywords = filter.keywords,
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns a [ContentFilter] from a
|
||||
* [v1 Mastodon filter][app.pachli.core.network.model.Filter].
|
||||
*
|
||||
* There are some restrictions imposed by the v1 filter;
|
||||
* - it can only have a single entry in the [keywords] list
|
||||
* - the [title] is identical to the keyword
|
||||
*/
|
||||
fun from(filter: app.pachli.core.network.model.FilterV1) = ContentFilter(
|
||||
id = filter.id,
|
||||
title = filter.phrase,
|
||||
contexts = filter.contexts,
|
||||
expiresAt = filter.expiresAt,
|
||||
filterAction = FilterAction.WARN,
|
||||
keywords = listOf(
|
||||
FilterKeyword(
|
||||
id = filter.id,
|
||||
keyword = filter.phrase,
|
||||
wholeWord = filter.wholeWord,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A new filter keyword; has no ID as it has not been saved to the server. */
|
||||
data class NewContentFilterKeyword(
|
||||
val keyword: String,
|
||||
val wholeWord: Boolean,
|
||||
fun ContentFilter.Companion.from(filter: NetworkFilter) = ContentFilter(
|
||||
id = filter.id,
|
||||
title = filter.title,
|
||||
contexts = filter.contexts.map { FilterContext.from(it) }.toSet(),
|
||||
expiresAt = filter.expiresAt,
|
||||
filterAction = FilterAction.from(filter.filterAction),
|
||||
keywords = filter.keywords.map { FilterKeyword.from(it) },
|
||||
)
|
||||
|
||||
fun FilterContext.Companion.from(networkFilter: NetworkFilterContext) =
|
||||
when (networkFilter) {
|
||||
NetworkFilterContext.HOME -> HOME
|
||||
NetworkFilterContext.NOTIFICATIONS -> NOTIFICATIONS
|
||||
NetworkFilterContext.PUBLIC -> PUBLIC
|
||||
NetworkFilterContext.THREAD -> THREAD
|
||||
NetworkFilterContext.ACCOUNT -> ACCOUNT
|
||||
}
|
||||
|
||||
fun FilterAction.Companion.from(networkAction: NetworkFilterAction) =
|
||||
when (networkAction) {
|
||||
NetworkFilterAction.NONE -> NONE
|
||||
NetworkFilterAction.WARN -> WARN
|
||||
NetworkFilterAction.HIDE -> HIDE
|
||||
}
|
||||
|
||||
fun FilterKeyword.Companion.from(networkKeyword: NetworkFilterKeyword) =
|
||||
FilterKeyword(
|
||||
id = networkKeyword.id,
|
||||
keyword = networkKeyword.keyword,
|
||||
wholeWord = networkKeyword.wholeWord,
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns a [ContentFilter] from a
|
||||
* [v1 Mastodon filter][app.pachli.core.network.model.Filter].
|
||||
*
|
||||
* There are some restrictions imposed by the v1 filter;
|
||||
* - it can only have a single entry in the [keywords] list
|
||||
* - the [title] is identical to the keyword
|
||||
*/
|
||||
fun ContentFilter.Companion.from(filter: NetworkFilterV1) = ContentFilter(
|
||||
id = filter.id,
|
||||
title = filter.phrase,
|
||||
contexts = filter.contexts.map { FilterContext.from(it) }.toSet(),
|
||||
expiresAt = filter.expiresAt,
|
||||
filterAction = WARN,
|
||||
keywords = listOf(
|
||||
FilterKeyword(
|
||||
id = filter.id,
|
||||
keyword = filter.phrase,
|
||||
wholeWord = filter.wholeWord,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -21,8 +21,7 @@ import androidx.annotation.VisibleForTesting
|
||||
import app.pachli.core.common.PachliError
|
||||
import app.pachli.core.common.di.ApplicationScope
|
||||
import app.pachli.core.data.R
|
||||
import app.pachli.core.data.model.ContentFilter
|
||||
import app.pachli.core.data.model.NewContentFilterKeyword
|
||||
import app.pachli.core.data.model.from
|
||||
import app.pachli.core.data.repository.ContentFiltersError.CreateContentFilterError
|
||||
import app.pachli.core.data.repository.ContentFiltersError.DeleteContentFilterError
|
||||
import app.pachli.core.data.repository.ContentFiltersError.GetContentFilterError
|
||||
@ -30,13 +29,17 @@ import app.pachli.core.data.repository.ContentFiltersError.GetContentFiltersErro
|
||||
import app.pachli.core.data.repository.ContentFiltersError.ServerDoesNotFilter
|
||||
import app.pachli.core.data.repository.ContentFiltersError.ServerRepositoryError
|
||||
import app.pachli.core.data.repository.ContentFiltersError.UpdateContentFilterError
|
||||
import app.pachli.core.model.ContentFilter
|
||||
import app.pachli.core.model.ContentFilterVersion
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.model.FilterContext
|
||||
import app.pachli.core.model.FilterKeyword
|
||||
import app.pachli.core.model.NewContentFilter
|
||||
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_FILTERS_CLIENT
|
||||
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_FILTERS_SERVER
|
||||
import app.pachli.core.network.Server
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.network.model.FilterContext
|
||||
import app.pachli.core.network.model.FilterKeyword
|
||||
import app.pachli.core.network.model.NewContentFilterV1
|
||||
import app.pachli.core.network.model.FilterAction as NetworkFilterAction
|
||||
import app.pachli.core.network.model.FilterContext as NetworkFilterContext
|
||||
import app.pachli.core.network.retrofit.MastodonApi
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
@ -56,45 +59,6 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
/**
|
||||
* A content filter to be created or updated.
|
||||
*
|
||||
* Same as [ContentFilter] except a [NewContentFilter] does not have an [id][ContentFilter.id], as it
|
||||
* has not been created on the server.
|
||||
*/
|
||||
data class NewContentFilter(
|
||||
val title: String,
|
||||
val contexts: Set<FilterContext>,
|
||||
val expiresIn: Int,
|
||||
val filterAction: FilterAction,
|
||||
val keywords: List<NewContentFilterKeyword>,
|
||||
) {
|
||||
fun toNewContentFilterV1() = this.keywords.map { keyword ->
|
||||
NewContentFilterV1(
|
||||
phrase = keyword.keyword,
|
||||
contexts = this.contexts,
|
||||
expiresIn = this.expiresIn,
|
||||
irreversible = false,
|
||||
wholeWord = keyword.wholeWord,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(contentFilter: ContentFilter) = NewContentFilter(
|
||||
title = contentFilter.title,
|
||||
contexts = contentFilter.contexts,
|
||||
expiresIn = -1,
|
||||
filterAction = contentFilter.filterAction,
|
||||
keywords = contentFilter.keywords.map {
|
||||
NewContentFilterKeyword(
|
||||
keyword = it.keyword,
|
||||
wholeWord = it.wholeWord,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a collection of edits to make to an existing content filter.
|
||||
*
|
||||
@ -153,11 +117,6 @@ sealed interface ContentFiltersError : PachliError {
|
||||
value class DeleteContentFilterError(private val error: PachliError) : ContentFiltersError, PachliError by error
|
||||
}
|
||||
|
||||
enum class ContentFilterVersion {
|
||||
V1,
|
||||
V2,
|
||||
}
|
||||
|
||||
// Hack, so that FilterModel can know whether this is V1 or V2 content filters.
|
||||
// See usage in:
|
||||
// - TimelineViewModel.getFilters()
|
||||
@ -248,28 +207,18 @@ class ContentFiltersRepository @Inject constructor(
|
||||
externalScope.async {
|
||||
when {
|
||||
server.canFilterV2() -> {
|
||||
mastodonApi.createFilter(
|
||||
title = filter.title,
|
||||
contexts = filter.contexts,
|
||||
filterAction = filter.filterAction,
|
||||
expiresInSeconds = expiresInSeconds,
|
||||
).andThen { response ->
|
||||
val filterId = response.body.id
|
||||
filter.keywords.mapResult {
|
||||
mastodonApi.addFilterKeyword(
|
||||
filterId,
|
||||
keyword = it.keyword,
|
||||
wholeWord = it.wholeWord,
|
||||
)
|
||||
}.map { ContentFilter.from(response.body) }
|
||||
mastodonApi.createFilter(filter).map {
|
||||
ContentFilter.from(it.body)
|
||||
}
|
||||
}
|
||||
|
||||
server.canFilterV1() -> {
|
||||
val networkContexts =
|
||||
filter.contexts.map { NetworkFilterContext.from(it) }.toSet()
|
||||
filter.toNewContentFilterV1().mapResult {
|
||||
mastodonApi.createFilterV1(
|
||||
phrase = it.phrase,
|
||||
context = it.contexts,
|
||||
context = networkContexts,
|
||||
irreversible = it.irreversible,
|
||||
wholeWord = it.wholeWord,
|
||||
expiresInSeconds = expiresInSeconds,
|
||||
@ -313,11 +262,18 @@ class ContentFiltersRepository @Inject constructor(
|
||||
contentFilterEdit.filterAction != null ||
|
||||
expiresInSeconds != null
|
||||
) {
|
||||
val networkContexts = contentFilterEdit.contexts?.map {
|
||||
NetworkFilterContext.from(it)
|
||||
}?.toSet()
|
||||
val networkAction = contentFilterEdit.filterAction?.let {
|
||||
NetworkFilterAction.from(it)
|
||||
}
|
||||
|
||||
mastodonApi.updateFilter(
|
||||
id = contentFilterEdit.id,
|
||||
title = contentFilterEdit.title,
|
||||
contexts = contentFilterEdit.contexts,
|
||||
filterAction = contentFilterEdit.filterAction,
|
||||
contexts = networkContexts,
|
||||
filterAction = networkAction,
|
||||
expiresInSeconds = expiresInSeconds,
|
||||
)
|
||||
} else {
|
||||
@ -352,11 +308,18 @@ class ContentFiltersRepository @Inject constructor(
|
||||
.map { ContentFilter.from(it.body) }
|
||||
}
|
||||
server.canFilterV1() -> {
|
||||
val networkContexts = contentFilterEdit.contexts?.map {
|
||||
NetworkFilterContext.from(it)
|
||||
}?.toSet() ?: originalContentFilter.contexts.map {
|
||||
NetworkFilterContext.from(
|
||||
it,
|
||||
)
|
||||
}
|
||||
mastodonApi.updateFilterV1(
|
||||
id = contentFilterEdit.id,
|
||||
phrase = contentFilterEdit.keywordsToModify?.firstOrNull()?.keyword ?: originalContentFilter.keywords.first().keyword,
|
||||
wholeWord = contentFilterEdit.keywordsToModify?.firstOrNull()?.wholeWord,
|
||||
contexts = contentFilterEdit.contexts ?: originalContentFilter.contexts,
|
||||
contexts = networkContexts,
|
||||
irreversible = false,
|
||||
expiresInSeconds = expiresInSeconds,
|
||||
).map { ContentFilter.from(it.body) }
|
||||
|
@ -18,12 +18,13 @@
|
||||
package app.pachli.core.data.repository.filtersRepository
|
||||
|
||||
import app.cash.turbine.test
|
||||
import app.pachli.core.data.model.NewContentFilterKeyword
|
||||
import app.pachli.core.data.repository.NewContentFilter
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.model.FilterContext
|
||||
import app.pachli.core.model.NewContentFilter
|
||||
import app.pachli.core.model.NewContentFilterKeyword
|
||||
import app.pachli.core.network.model.Filter as NetworkFilter
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.network.model.FilterContext
|
||||
import app.pachli.core.network.model.FilterKeyword
|
||||
import app.pachli.core.network.model.FilterAction as NetworkFilterAction
|
||||
import app.pachli.core.network.model.FilterContext as NetworkFilterContext
|
||||
import app.pachli.core.network.model.FilterV1 as NetworkFilterV1
|
||||
import app.pachli.core.testing.success
|
||||
import com.github.michaelbull.result.Ok
|
||||
@ -57,27 +58,30 @@ class ContentFiltersRepositoryTestCreate : BaseContentFiltersRepositoryTest() {
|
||||
fun `creating v2 filter should send correct requests`() = runTest {
|
||||
mastodonApi.stub {
|
||||
onBlocking { getContentFilters() } doReturn success(emptyList())
|
||||
onBlocking { createFilter(any(), any(), any(), any()) } doAnswer { call ->
|
||||
onBlocking { createFilter(any<NewContentFilter>()) } doAnswer { call ->
|
||||
success(
|
||||
NetworkFilter(
|
||||
id = "1",
|
||||
title = call.getArgument(0),
|
||||
contexts = call.getArgument(1),
|
||||
filterAction = call.getArgument(2),
|
||||
expiresAt = Date(System.currentTimeMillis() + (call.getArgument<String>(3).toInt() * 1000)),
|
||||
title = call.getArgument<NewContentFilter>(0).title,
|
||||
contexts = call.getArgument<NewContentFilter>(0).contexts.map {
|
||||
NetworkFilterContext.from(it)
|
||||
}.toSet(),
|
||||
filterAction = NetworkFilterAction.from(
|
||||
call.getArgument<NewContentFilter>(
|
||||
0,
|
||||
).filterAction,
|
||||
),
|
||||
expiresAt = Date(
|
||||
System.currentTimeMillis() + (
|
||||
call.getArgument<NewContentFilter>(
|
||||
0,
|
||||
).expiresIn * 1000
|
||||
),
|
||||
),
|
||||
keywords = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
onBlocking { addFilterKeyword(any(), any(), any()) } doAnswer { call ->
|
||||
success(
|
||||
FilterKeyword(
|
||||
id = "1",
|
||||
keyword = call.getArgument(1),
|
||||
wholeWord = call.getArgument(2),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
contentFiltersRepository.contentFilters.test {
|
||||
@ -87,16 +91,7 @@ class ContentFiltersRepositoryTestCreate : BaseContentFiltersRepositoryTest() {
|
||||
advanceUntilIdle()
|
||||
|
||||
// createFilter should have been called once, with the correct arguments.
|
||||
verify(mastodonApi, times(1)).createFilter(
|
||||
title = filterWithTwoKeywords.title,
|
||||
contexts = filterWithTwoKeywords.contexts,
|
||||
filterAction = filterWithTwoKeywords.filterAction,
|
||||
expiresInSeconds = filterWithTwoKeywords.expiresIn.toString(),
|
||||
)
|
||||
|
||||
// To create the keywords addFilterKeyword should have been called twice.
|
||||
verify(mastodonApi, times(1)).addFilterKeyword("1", "first", false)
|
||||
verify(mastodonApi, times(1)).addFilterKeyword("1", "second", true)
|
||||
verify(mastodonApi, times(1)).createFilter(filterWithTwoKeywords)
|
||||
|
||||
// Filters should have been refreshed
|
||||
verify(mastodonApi, times(2)).getContentFilters()
|
||||
@ -110,27 +105,24 @@ class ContentFiltersRepositoryTestCreate : BaseContentFiltersRepositoryTest() {
|
||||
fun `expiresIn of 0 is converted to empty string`() = runTest {
|
||||
mastodonApi.stub {
|
||||
onBlocking { getContentFilters() } doReturn success(emptyList())
|
||||
onBlocking { createFilter(any(), any(), any(), any()) } doAnswer { call ->
|
||||
onBlocking { createFilter(any()) } doAnswer { call ->
|
||||
success(
|
||||
NetworkFilter(
|
||||
id = "1",
|
||||
title = call.getArgument(0),
|
||||
contexts = call.getArgument(1),
|
||||
filterAction = call.getArgument(2),
|
||||
title = call.getArgument<NewContentFilter>(0).title,
|
||||
contexts = call.getArgument<NewContentFilter>(0).contexts.map {
|
||||
NetworkFilterContext.from(it)
|
||||
}.toSet(),
|
||||
filterAction = NetworkFilterAction.from(
|
||||
call.getArgument<NewContentFilter>(
|
||||
0,
|
||||
).filterAction,
|
||||
),
|
||||
expiresAt = null,
|
||||
keywords = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
onBlocking { addFilterKeyword(any(), any(), any()) } doAnswer { call ->
|
||||
success(
|
||||
FilterKeyword(
|
||||
id = "1",
|
||||
keyword = call.getArgument(1),
|
||||
wholeWord = call.getArgument(2),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// The v2 filter creation test covers most things, this just verifies that
|
||||
@ -143,12 +135,7 @@ class ContentFiltersRepositoryTestCreate : BaseContentFiltersRepositoryTest() {
|
||||
contentFiltersRepository.createContentFilter(filterWithZeroExpiry)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify(mastodonApi, times(1)).createFilter(
|
||||
title = filterWithZeroExpiry.title,
|
||||
contexts = filterWithZeroExpiry.contexts,
|
||||
filterAction = filterWithZeroExpiry.filterAction,
|
||||
expiresInSeconds = "",
|
||||
)
|
||||
verify(mastodonApi, times(1)).createFilter(filterWithZeroExpiry)
|
||||
|
||||
cancelAndConsumeRemainingEvents()
|
||||
}
|
||||
@ -185,7 +172,9 @@ class ContentFiltersRepositoryTestCreate : BaseContentFiltersRepositoryTest() {
|
||||
filterWithTwoKeywords.keywords.forEach { keyword ->
|
||||
verify(mastodonApi, times(1)).createFilterV1(
|
||||
phrase = keyword.keyword,
|
||||
context = filterWithTwoKeywords.contexts,
|
||||
context = filterWithTwoKeywords.contexts.map {
|
||||
NetworkFilterContext.from(it)
|
||||
}.toSet(),
|
||||
irreversible = false,
|
||||
wholeWord = keyword.wholeWord,
|
||||
expiresInSeconds = filterWithTwoKeywords.expiresIn.toString(),
|
||||
|
@ -18,15 +18,17 @@
|
||||
package app.pachli.core.data.repository.filtersRepository
|
||||
|
||||
import app.cash.turbine.test
|
||||
import app.pachli.core.data.model.ContentFilter
|
||||
import app.pachli.core.data.repository.ContentFilterVersion.V1
|
||||
import app.pachli.core.data.repository.ContentFilterVersion.V2
|
||||
import app.pachli.core.data.repository.ContentFilters
|
||||
import app.pachli.core.data.repository.ContentFiltersError
|
||||
import app.pachli.core.model.ContentFilter
|
||||
import app.pachli.core.model.ContentFilterVersion
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.model.FilterContext
|
||||
import app.pachli.core.model.FilterKeyword
|
||||
import app.pachli.core.network.model.Filter as NetworkFilter
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.network.model.FilterContext
|
||||
import app.pachli.core.network.model.FilterKeyword
|
||||
import app.pachli.core.network.model.FilterAction as NetworkFilterAction
|
||||
import app.pachli.core.network.model.FilterContext as NetworkFilterContext
|
||||
import app.pachli.core.network.model.FilterKeyword as NetworkFilterKeyword
|
||||
import app.pachli.core.network.model.FilterV1 as NetworkFilterV1
|
||||
import app.pachli.core.network.retrofit.apiresult.ClientError
|
||||
import app.pachli.core.testing.failure
|
||||
@ -57,7 +59,12 @@ class ContentFiltersRepositoryTestFlow : BaseContentFiltersRepositoryTest() {
|
||||
advanceUntilIdle()
|
||||
val item = expectMostRecentItem()
|
||||
val filters = item.get()
|
||||
assertThat(filters).isEqualTo(ContentFilters(version = V2, contentFilters = emptyList()))
|
||||
assertThat(filters).isEqualTo(
|
||||
ContentFilters(
|
||||
version = ContentFilterVersion.V2,
|
||||
contentFilters = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,10 +79,16 @@ class ContentFiltersRepositoryTestFlow : BaseContentFiltersRepositoryTest() {
|
||||
NetworkFilter(
|
||||
id = "1",
|
||||
title = "test filter",
|
||||
contexts = setOf(FilterContext.HOME),
|
||||
filterAction = FilterAction.WARN,
|
||||
contexts = setOf(NetworkFilterContext.HOME),
|
||||
filterAction = NetworkFilterAction.WARN,
|
||||
expiresAt = expiresAt,
|
||||
keywords = listOf(FilterKeyword(id = "1", keyword = "foo", wholeWord = true)),
|
||||
keywords = listOf(
|
||||
NetworkFilterKeyword(
|
||||
id = "1",
|
||||
keyword = "foo",
|
||||
wholeWord = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -87,7 +100,7 @@ class ContentFiltersRepositoryTestFlow : BaseContentFiltersRepositoryTest() {
|
||||
val filters = item.get()
|
||||
assertThat(filters).isEqualTo(
|
||||
ContentFilters(
|
||||
version = V2,
|
||||
version = ContentFilterVersion.V2,
|
||||
contentFilters = listOf(
|
||||
ContentFilter(
|
||||
id = "1",
|
||||
@ -117,7 +130,12 @@ class ContentFiltersRepositoryTestFlow : BaseContentFiltersRepositoryTest() {
|
||||
advanceUntilIdle()
|
||||
val item = expectMostRecentItem()
|
||||
val filters = item.get()
|
||||
assertThat(filters).isEqualTo(ContentFilters(version = V1, contentFilters = emptyList()))
|
||||
assertThat(filters).isEqualTo(
|
||||
ContentFilters(
|
||||
version = ContentFilterVersion.V1,
|
||||
contentFilters = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,7 +150,7 @@ class ContentFiltersRepositoryTestFlow : BaseContentFiltersRepositoryTest() {
|
||||
NetworkFilterV1(
|
||||
id = "1",
|
||||
phrase = "some_phrase",
|
||||
contexts = setOf(FilterContext.HOME),
|
||||
contexts = setOf(NetworkFilterContext.HOME),
|
||||
expiresAt = expiresAt,
|
||||
irreversible = true,
|
||||
wholeWord = true,
|
||||
@ -149,7 +167,7 @@ class ContentFiltersRepositoryTestFlow : BaseContentFiltersRepositoryTest() {
|
||||
val filters = item.get()
|
||||
assertThat(filters).isEqualTo(
|
||||
ContentFilters(
|
||||
version = V1,
|
||||
version = ContentFilterVersion.V1,
|
||||
contentFilters = listOf(
|
||||
ContentFilter(
|
||||
id = "1",
|
||||
|
@ -18,12 +18,14 @@
|
||||
package app.pachli.core.data.repository.filtersRepository
|
||||
|
||||
import app.cash.turbine.test
|
||||
import app.pachli.core.data.model.ContentFilter
|
||||
import app.pachli.core.data.model.from
|
||||
import app.pachli.core.data.repository.ContentFilterEdit
|
||||
import app.pachli.core.model.ContentFilter
|
||||
import app.pachli.core.model.FilterKeyword
|
||||
import app.pachli.core.network.model.Filter as NetworkFilter
|
||||
import app.pachli.core.network.model.FilterAction
|
||||
import app.pachli.core.network.model.FilterContext
|
||||
import app.pachli.core.network.model.FilterKeyword
|
||||
import app.pachli.core.network.model.FilterAction as NetworkFilterAction
|
||||
import app.pachli.core.network.model.FilterContext as NetworkFilterContext
|
||||
import app.pachli.core.network.model.FilterKeyword as NetworkFilterKeyword
|
||||
import app.pachli.core.testing.success
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import java.util.Date
|
||||
@ -48,14 +50,14 @@ class ContentFiltersRepositoryTestUpdate : BaseContentFiltersRepositoryTest() {
|
||||
private val originalNetworkFilter = NetworkFilter(
|
||||
id = "1",
|
||||
title = "original filter",
|
||||
contexts = setOf(FilterContext.HOME),
|
||||
contexts = setOf(NetworkFilterContext.HOME),
|
||||
expiresAt = null,
|
||||
filterAction = FilterAction.WARN,
|
||||
filterAction = NetworkFilterAction.WARN,
|
||||
keywords = listOf(
|
||||
FilterKeyword(id = "1", keyword = "first", wholeWord = false),
|
||||
FilterKeyword(id = "2", keyword = "second", wholeWord = true),
|
||||
FilterKeyword(id = "3", keyword = "three", wholeWord = true),
|
||||
FilterKeyword(id = "4", keyword = "four", wholeWord = true),
|
||||
NetworkFilterKeyword(id = "1", keyword = "first", wholeWord = false),
|
||||
NetworkFilterKeyword(id = "2", keyword = "second", wholeWord = true),
|
||||
NetworkFilterKeyword(id = "3", keyword = "three", wholeWord = true),
|
||||
NetworkFilterKeyword(id = "4", keyword = "four", wholeWord = true),
|
||||
),
|
||||
)
|
||||
|
||||
@ -69,8 +71,8 @@ class ContentFiltersRepositoryTestUpdate : BaseContentFiltersRepositoryTest() {
|
||||
success(
|
||||
originalNetworkFilter.copy(
|
||||
title = call.getArgument(1) ?: originalContentFilter.title,
|
||||
contexts = call.getArgument(2) ?: originalContentFilter.contexts,
|
||||
filterAction = call.getArgument(3) ?: originalContentFilter.filterAction,
|
||||
contexts = call.getArgument(2) ?: originalContentFilter.contexts.map { NetworkFilterContext.from(it) }.toSet(),
|
||||
filterAction = call.getArgument(3) ?: NetworkFilterAction.from(originalContentFilter.filterAction),
|
||||
expiresAt = call.getArgument<String?>(4)?.let {
|
||||
when (it) {
|
||||
"" -> null
|
||||
@ -95,8 +97,8 @@ class ContentFiltersRepositoryTestUpdate : BaseContentFiltersRepositoryTest() {
|
||||
verify(mastodonApi, times(1)).updateFilter(
|
||||
id = update.id,
|
||||
title = update.title,
|
||||
contexts = update.contexts,
|
||||
filterAction = update.filterAction,
|
||||
contexts = update.contexts?.map { NetworkFilterContext.from(it) }?.toSet(),
|
||||
filterAction = update.filterAction?.let { NetworkFilterAction.from(it) },
|
||||
expiresInSeconds = null,
|
||||
)
|
||||
|
||||
@ -114,10 +116,10 @@ class ContentFiltersRepositoryTestUpdate : BaseContentFiltersRepositoryTest() {
|
||||
onBlocking { getContentFilters() } doReturn success(emptyList())
|
||||
onBlocking { deleteFilterKeyword(any()) } doReturn success(Unit)
|
||||
onBlocking { updateFilterKeyword(any(), any(), any()) } doAnswer { call ->
|
||||
success(FilterKeyword(call.getArgument(0), call.getArgument(1), call.getArgument(2)))
|
||||
success(NetworkFilterKeyword(call.getArgument(0), call.getArgument(1), call.getArgument(2)))
|
||||
}
|
||||
onBlocking { addFilterKeyword(any(), any(), any()) } doAnswer { call ->
|
||||
success(FilterKeyword("x", call.getArgument(1), call.getArgument(2)))
|
||||
success(NetworkFilterKeyword("x", call.getArgument(1), call.getArgument(2)))
|
||||
}
|
||||
onBlocking { getFilter(any()) } doReturn success(originalNetworkFilter)
|
||||
}
|
||||
|
@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.util.Date
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
enum class ContentFilterVersion {
|
||||
V1,
|
||||
V2,
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal representation of a Mastodon filter, whether v1 or v2.
|
||||
*
|
||||
* This is a *content* filter, to distinguish it from filters that operate on
|
||||
* accounts, domains, or other data.
|
||||
*
|
||||
* @param id The server's ID for this filter
|
||||
* @param title Filter's title (label to use in the UI)
|
||||
* @param contexts One or more [FilterContext] the filter is applied to
|
||||
* @param expiresAt Date the filter expires, null if the filter does not expire
|
||||
* @param filterAction Action to take if the filter matches a status
|
||||
* @param keywords One or more [FilterKeyword] the filter matches against a status
|
||||
*/
|
||||
// The @JsonClass annotations are used when this is serialized to the database.
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ContentFilter(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val contexts: Set<FilterContext> = emptySet(),
|
||||
val expiresAt: Date? = null,
|
||||
val filterAction: FilterAction,
|
||||
val keywords: List<FilterKeyword> = emptyList(),
|
||||
) : Parcelable {
|
||||
companion object {}
|
||||
}
|
||||
|
||||
/** A filter choice, either content filter or account filter. */
|
||||
// The @Json annotations are used when this is serialized by NewContentFilterConverterFactory.
|
||||
enum class FilterAction {
|
||||
/** No filtering, show item as normal. */
|
||||
@Json(name = "none")
|
||||
NONE,
|
||||
|
||||
/** Replace the item with a warning, allowing the user to click through. */
|
||||
@Json(name = "warn")
|
||||
WARN,
|
||||
|
||||
/** Remove the item, with no indication to the user it was present. */
|
||||
@Json(name = "hide")
|
||||
HIDE,
|
||||
|
||||
;
|
||||
|
||||
companion object
|
||||
}
|
||||
|
||||
// The @JsonClass annotations are used when this is serialized to the database.
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FilterKeyword(
|
||||
val id: String,
|
||||
val keyword: String,
|
||||
@Json(name = "whole_word") val wholeWord: Boolean,
|
||||
) : Parcelable {
|
||||
companion object
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
// The @Json annotations are used when this is serialized by NewContentFilterConverterFactory
|
||||
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 */
|
||||
@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 {
|
||||
/**
|
||||
* @return The filter context for [timeline], or null if filters are not applied
|
||||
* to this timeline.
|
||||
*/
|
||||
fun from(timeline: Timeline): FilterContext? = when (timeline) {
|
||||
is Timeline.Home, is Timeline.UserList -> HOME
|
||||
is Timeline.User -> ACCOUNT
|
||||
Timeline.Notifications -> NOTIFICATIONS
|
||||
Timeline.Bookmarks,
|
||||
Timeline.Favourites,
|
||||
Timeline.PublicFederated,
|
||||
Timeline.PublicLocal,
|
||||
is Timeline.Hashtags,
|
||||
Timeline.TrendingStatuses,
|
||||
Timeline.TrendingHashtags,
|
||||
Timeline.TrendingLinks,
|
||||
-> PUBLIC
|
||||
Timeline.Conversations -> null
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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.model
|
||||
|
||||
/**
|
||||
* A content filter to be created or updated.
|
||||
*
|
||||
* Same as [ContentFilter] except a [NewContentFilter] does not have an [id][ContentFilter.id], as it
|
||||
* has not been created on the server.
|
||||
*/
|
||||
data class NewContentFilter(
|
||||
val title: String,
|
||||
val contexts: Set<FilterContext>,
|
||||
val expiresIn: Int,
|
||||
val filterAction: FilterAction,
|
||||
val keywords: List<NewContentFilterKeyword>,
|
||||
) {
|
||||
fun toNewContentFilterV1() = this.keywords.map { keyword ->
|
||||
NewContentFilterV1(
|
||||
phrase = keyword.keyword,
|
||||
contexts = this.contexts,
|
||||
expiresIn = this.expiresIn,
|
||||
irreversible = false,
|
||||
wholeWord = keyword.wholeWord,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(contentFilter: ContentFilter) = NewContentFilter(
|
||||
title = contentFilter.title,
|
||||
contexts = contentFilter.contexts,
|
||||
expiresIn = -1,
|
||||
filterAction = contentFilter.filterAction,
|
||||
keywords = contentFilter.keywords.map {
|
||||
NewContentFilterKeyword(
|
||||
keyword = it.keyword,
|
||||
wholeWord = it.wholeWord,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A new filter keyword; has no ID as it has not been saved to the server. */
|
||||
data class NewContentFilterKeyword(
|
||||
val keyword: String,
|
||||
val wholeWord: Boolean,
|
||||
)
|
||||
|
||||
data class NewContentFilterV1(
|
||||
val phrase: String,
|
||||
val contexts: Set<FilterContext>,
|
||||
val expiresIn: Int,
|
||||
val irreversible: Boolean,
|
||||
val wholeWord: Boolean,
|
||||
)
|
@ -21,8 +21,8 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Parcelable
|
||||
import androidx.core.content.IntentCompat
|
||||
import app.pachli.core.data.model.ContentFilter
|
||||
import app.pachli.core.database.model.DraftAttachment
|
||||
import app.pachli.core.model.ContentFilter
|
||||
import app.pachli.core.model.Timeline
|
||||
import app.pachli.core.navigation.LoginActivityIntent.LoginMode
|
||||
import app.pachli.core.navigation.TimelineActivityIntent.Companion.bookmarks
|
||||
|
@ -29,6 +29,7 @@ import app.pachli.core.network.json.HasDefault
|
||||
import app.pachli.core.network.model.MediaUploadApi
|
||||
import app.pachli.core.network.retrofit.InstanceSwitchAuthInterceptor
|
||||
import app.pachli.core.network.retrofit.MastodonApi
|
||||
import app.pachli.core.network.retrofit.NewContentFilterConverterFactory
|
||||
import app.pachli.core.network.retrofit.apiresult.ApiResultCallAdapterFactory
|
||||
import app.pachli.core.network.util.localHandshakeCertificates
|
||||
import app.pachli.core.preferences.PrefKeys.HTTP_PROXY_ENABLED
|
||||
@ -138,6 +139,7 @@ object NetworkModule {
|
||||
return Retrofit.Builder().baseUrl("https://" + MastodonApi.PLACEHOLDER_DOMAIN)
|
||||
.client(httpClient)
|
||||
.addConverterFactory(EnumConstantConverterFactory)
|
||||
.addConverterFactory(NewContentFilterConverterFactory)
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.addCallAdapterFactory(ApiResultCallAdapterFactory.create())
|
||||
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
package app.pachli.core.network.model
|
||||
|
||||
import app.pachli.core.model.FilterAction
|
||||
import app.pachli.core.network.json.Default
|
||||
import app.pachli.core.network.json.HasDefault
|
||||
import com.squareup.moshi.Json
|
||||
@ -36,4 +37,14 @@ enum class FilterAction {
|
||||
/** Remove the item, with no indication to the user it was present. */
|
||||
@Json(name = "hide")
|
||||
HIDE,
|
||||
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun from(filterAction: app.pachli.core.model.FilterAction) = when (filterAction) {
|
||||
FilterAction.NONE -> NONE
|
||||
FilterAction.WARN -> WARN
|
||||
FilterAction.HIDE -> HIDE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +60,7 @@ enum class FilterContext {
|
||||
* @return The filter context for [timeline], or null if filters are not applied
|
||||
* to this timeline.
|
||||
*/
|
||||
@Deprecated("Use app.pachli.core.model.FilterContext instead")
|
||||
fun from(timeline: Timeline): FilterContext? = when (timeline) {
|
||||
is Timeline.Home, is Timeline.UserList -> HOME
|
||||
is Timeline.User -> ACCOUNT
|
||||
@ -75,5 +76,13 @@ enum class FilterContext {
|
||||
-> PUBLIC
|
||||
Timeline.Conversations -> null
|
||||
}
|
||||
|
||||
fun from(filterContext: app.pachli.core.model.FilterContext) = when (filterContext) {
|
||||
app.pachli.core.model.FilterContext.HOME -> HOME
|
||||
app.pachli.core.model.FilterContext.NOTIFICATIONS -> NOTIFICATIONS
|
||||
app.pachli.core.model.FilterContext.PUBLIC -> PUBLIC
|
||||
app.pachli.core.model.FilterContext.THREAD -> THREAD
|
||||
app.pachli.core.model.FilterContext.ACCOUNT -> ACCOUNT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
package app.pachli.core.network.retrofit
|
||||
|
||||
import app.pachli.core.model.NewContentFilter
|
||||
import app.pachli.core.network.model.AccessToken
|
||||
import app.pachli.core.network.model.Account
|
||||
import app.pachli.core.network.model.Announcement
|
||||
@ -649,16 +650,8 @@ interface MastodonApi {
|
||||
@Path("id") id: String,
|
||||
): ApiResult<Unit>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v2/filters")
|
||||
suspend fun createFilter(
|
||||
@Field("title") title: String,
|
||||
@Field("context[]") contexts: Set<FilterContext>,
|
||||
@Field("filter_action") filterAction: FilterAction,
|
||||
// String not Int because the empty string is used to represent "indefinite",
|
||||
// see https://github.com/mastodon/documentation/issues/1216#issuecomment-2030222940
|
||||
@Field("expires_in") expiresInSeconds: String?,
|
||||
): ApiResult<Filter>
|
||||
suspend fun createFilter(@Body newContentFilter: NewContentFilter): ApiResult<Filter>
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT("api/v2/filters/{id}")
|
||||
|
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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.retrofit
|
||||
|
||||
import app.pachli.core.model.NewContentFilter
|
||||
import app.pachli.core.network.json.EnumConstantConverterFactory
|
||||
import java.lang.reflect.Type
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
|
||||
/**
|
||||
* Converts a [NewContentFilter] to a request body.
|
||||
*
|
||||
* Should be added to Retrofit using [addConverterFactory][retrofit2.Retrofit.Builder.addConverterFactory].
|
||||
*/
|
||||
// Retrofit can't do this natively because it can't handle fields like "keywords_attributes"
|
||||
// that repeat multiple times with the same names.
|
||||
object NewContentFilterConverterFactory : Converter.Factory() {
|
||||
object NewContentFilterConverter : Converter<NewContentFilter, RequestBody> {
|
||||
private val enumConverter = EnumConstantConverterFactory.EnumConstantConverter
|
||||
private val utf8 = StandardCharsets.UTF_8.toString()
|
||||
|
||||
override fun convert(newContentFilter: NewContentFilter): RequestBody {
|
||||
return buildList {
|
||||
add("title=${encode(newContentFilter.title)}")
|
||||
|
||||
newContentFilter.contexts.forEach {
|
||||
add("context[]=${encode(enumConverter.convert(it))}")
|
||||
}
|
||||
|
||||
add("filter_action=${encode(enumConverter.convert(newContentFilter.filterAction))}")
|
||||
|
||||
if (newContentFilter.expiresIn != 0) {
|
||||
add("expires_in=${newContentFilter.expiresIn}")
|
||||
}
|
||||
|
||||
newContentFilter.keywords.forEach {
|
||||
add("keywords_attributes[][keyword]=${encode(it.keyword)}")
|
||||
add("keywords_attributes[][whole_word]=${it.wholeWord}")
|
||||
}
|
||||
}.joinToString("&").toRequestBody()
|
||||
}
|
||||
|
||||
/** @return URL encoded [s] using UTF8 as the character encoding. */
|
||||
private fun encode(s: String) = URLEncoder.encode(s, utf8)
|
||||
}
|
||||
|
||||
override fun requestBodyConverter(
|
||||
type: Type,
|
||||
parameterAnnotations: Array<out Annotation>,
|
||||
methodAnnotations: Array<out Annotation>,
|
||||
retrofit: Retrofit,
|
||||
): Converter<*, RequestBody>? {
|
||||
return if (type == NewContentFilter::class.java) NewContentFilterConverter else null
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user