feat(Scout): add authorType filter

whew, the code is getting quite big... A refactor will be coming soon

Some light linting was also done here
This commit is contained in:
Cilly Leang 2026-02-18 03:14:40 +11:00
parent 302ea0094a
commit e4ab9f936d
Signed by: cilly
GPG key ID: 6500251E087653C9
12 changed files with 347 additions and 37 deletions

View file

@ -9,6 +9,7 @@ object FilterTypeExtension {
lateinit var DURING: FilterType
lateinit var AFTER: FilterType
lateinit var EXCLUDE: FilterType
lateinit var AUTHOR_TYPE: FilterType
lateinit var dates: Array<FilterType>
lateinit var filters: Array<FilterType>
lateinit var values: Array<FilterType>

View file

@ -1,8 +1,17 @@
@file:Suppress("EnumValuesSoftDeprecate", "CanConvertToMultiDollarString")
/**
* Hi to anyone who might be reading this; I am sorry for the atrocious code in this plugin
* but I promise I'll be fixing it up soon :3
*/
package moe.lava.awoocord.scout
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Resources
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.core.content.res.ResourcesCompat
import com.aliucord.Utils
@ -27,6 +36,7 @@ import com.discord.api.channel.Channel
import com.discord.api.channel.ChannelUtils
import com.discord.api.channel.`ChannelUtils$getSortByNameAndType$1`
import com.discord.api.permission.Permission
import com.discord.databinding.WidgetSearchSuggestionItemHeaderBinding
import com.discord.databinding.WidgetSearchSuggestionsItemHasBinding
import com.discord.databinding.WidgetSearchSuggestionsItemSuggestionBinding
import com.discord.models.member.GuildMember
@ -71,6 +81,10 @@ import com.franmontiel.persistentcookiejar.cache.SetCookieCache
import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor
import com.lytefast.flexinput.R
import moe.lava.awoocord.scout.api.SearchAPIInterface
import moe.lava.awoocord.scout.entries.AuthorTypeSuggestion
import moe.lava.awoocord.scout.entries.AuthorTypeViewHolder
import moe.lava.awoocord.scout.parsing.AuthorType
import moe.lava.awoocord.scout.parsing.AuthorTypeNode
import moe.lava.awoocord.scout.parsing.DateNode
import moe.lava.awoocord.scout.parsing.SimpleParserRule
import moe.lava.awoocord.scout.parsing.SortNode
@ -84,7 +98,10 @@ import b.a.k.b as FormatUtils
private val WidgetSearchSuggestionsAdapter.FilterViewHolder.binding
by accessField<WidgetSearchSuggestionsItemSuggestionBinding>()
@AliucordPlugin()
private val WidgetSearchSuggestionsAdapter.HeaderViewHolder.binding
by accessField<WidgetSearchSuggestionItemHeaderBinding>()
@AliucordPlugin
@Suppress("unused", "unchecked_cast")
class Scout : Plugin() {
lateinit var scoutRes: ScoutResource
@ -107,6 +124,7 @@ class Scout : Plugin() {
override fun start(context: Context) {
extendFilterType()
extendHasAnswerOption()
extendSuggestionCategory()
fixFiltersKeying()
fixHasFilterSuggestion()
fixSearchPadding()
@ -120,9 +138,10 @@ class Scout : Plugin() {
}
override fun stop(context: Context) {
patcher.unpatchAll()
resetFilterType()
resetHasAnswerOption()
patcher.unpatchAll()
resetSuggestionCategory()
}
// Creates a new custom search API implementation, for the extra `min_id` param in search queries
@ -148,7 +167,7 @@ class Scout : Plugin() {
private var origFilterTypes: Array<FilterType>? = null
// Creates new pseudo-values of the `FilterType` enum for date filters
@Suppress("LocalVariableName")
@Suppress("LocalVariableName", "AssignedValueIsNeverRead")
private fun extendFilterType() {
val cls = FilterType::class.java
val constructor = cls.declaredConstructors[0]
@ -163,18 +182,20 @@ class Scout : Plugin() {
val EXPAND = constructor.newInstance("EXPAND", nextIdx++) as FilterType
val SORT = constructor.newInstance("SORT", nextIdx++) as FilterType
val EXCLUDE = constructor.newInstance("EXCLUDE", nextIdx++) as FilterType
val AUTHOR_TYPE = constructor.newInstance("AUTHOR_TYPE", nextIdx++) as FilterType
val BEFORE = constructor.newInstance("BEFORE", nextIdx++) as FilterType
val DURING = constructor.newInstance("DURING", nextIdx++) as FilterType
val AFTER = constructor.newInstance("AFTER", nextIdx++) as FilterType
FilterTypeExtension.EXPAND = EXPAND
FilterTypeExtension.SORT = SORT
FilterTypeExtension.EXCLUDE = EXCLUDE
FilterTypeExtension.AUTHOR_TYPE = AUTHOR_TYPE
FilterTypeExtension.BEFORE = BEFORE
FilterTypeExtension.DURING = DURING
FilterTypeExtension.AFTER = AFTER
FilterTypeExtension.dates = arrayOf(BEFORE, DURING, AFTER)
FilterTypeExtension.values = arrayOf(EXPAND, SORT, EXCLUDE, BEFORE, DURING, AFTER)
FilterTypeExtension.filters = arrayOf(SORT, EXCLUDE, BEFORE, DURING, AFTER)
FilterTypeExtension.filters = arrayOf(SORT, AUTHOR_TYPE, EXCLUDE) + FilterTypeExtension.dates
FilterTypeExtension.values = arrayOf(EXPAND) + FilterTypeExtension.filters
val newValues = values.toMutableList()
newValues.addAll(FilterTypeExtension.values)
@ -194,7 +215,7 @@ class Scout : Plugin() {
private var origHasAnswerOptions: Array<HasAnswerOption>? = null
// Creates new pseudo-values of the `HasAnswerOption` enum for poll and forwarded filters
@Suppress("LocalVariableName")
@Suppress("LocalVariableName", "AssignedValueIsNeverRead")
private fun extendHasAnswerOption() {
val cls = HasAnswerOption::class.java
val constructor = cls.declaredConstructors[0]
@ -207,7 +228,7 @@ class Scout : Plugin() {
var nextIdx = values.size
val POLL = constructor.newInstance("POLL", nextIdx++, "poll") as HasAnswerOption
val SNAPSHOT = constructor.newInstance("SNAPSHOT", nextIdx, "snapshot") as HasAnswerOption
val SNAPSHOT = constructor.newInstance("SNAPSHOT", nextIdx++, "snapshot") as HasAnswerOption
HasAnswerOptionExtension.POLL = POLL
HasAnswerOptionExtension.SNAPSHOT = SNAPSHOT
HasAnswerOptionExtension.values = arrayOf(POLL, SNAPSHOT)
@ -228,6 +249,40 @@ class Scout : Plugin() {
origHasAnswerOptions = null
}
private var origSuggestionCategories: Array<SearchSuggestion.Category>? = null
// Creates new pseudo-values of the suggestion categories to add correct headers
@Suppress("LocalVariableName", "AssignedValueIsNeverRead")
private fun extendSuggestionCategory() {
val cls = SearchSuggestion.Category::class.java
val constructor = cls.declaredConstructors[0]
constructor.isAccessible = true
val field = cls.getDeclaredField("\$VALUES")
field.isAccessible = true
val values = field.get(null) as Array<SearchSuggestion.Category>
origSuggestionCategories = origSuggestionCategories ?: values
var nextIdx = values.size
val AUTHOR_TYPE = constructor.newInstance("AUTHOR_TYPE", nextIdx++) as SearchSuggestion.Category
SuggestionCategoryExtension.AUTHOR_TYPE = AUTHOR_TYPE
SuggestionCategoryExtension.values = arrayOf(AUTHOR_TYPE)
val newValues = values.toMutableList()
newValues.addAll(SuggestionCategoryExtension.values)
field.set(null, newValues.toTypedArray())
}
private fun resetSuggestionCategory() {
if (origSuggestionCategories == null)
return logger.error("No unpatched suggestion categories?", null)
val cls = SearchSuggestion.Category::class.java
val field = cls.getDeclaredField("\$VALUES")
field.isAccessible = true
field.set(null, origSuggestionCategories)
origSuggestionCategories = null
}
// Patch to key filters properly for smoother recycling
// Thank u discord for keying every filter type the same thing!! /s
private fun fixFiltersKeying() {
@ -341,24 +396,22 @@ class Scout : Plugin() {
CharSequence::class.java,
FilterType::class.java,
SearchStringProvider::class.java,
) { param ->
val query = param.args[0] as CharSequence
val filterType = param.args[1] as FilterType
val ossProvider = param.args[2] as SearchStringProvider
if (filterType != FilterType.HAS && filterType != FilterTypeExtension.EXCLUDE)
return@instead listOf<Any>()
val res = mutableListOf<HasSuggestion>()
for (opt in HasAnswerOption.values()) {
val filterText = opt.getLocalizedInputText(ossProvider)
if (filterText.contains(query))
res.add(HasSuggestion(opt))
) { (_, query: CharSequence, type: FilterType, provider: SearchStringProvider) ->
// Generate entries for author type
if (type == FilterTypeExtension.AUTHOR_TYPE) {
return@instead AuthorType.values()
.filter { it.value.contains(query) }
.map { AuthorTypeSuggestion(it) }
}
res.toList()
}
// Generate entries for has options, including new ones
if (type == FilterType.HAS || type == FilterTypeExtension.EXCLUDE)
return@instead HasAnswerOption.values()
.filter { it.getLocalizedInputText(provider).contains(query) }
.map { HasSuggestion(it) }
listOf<Any>()
}
}
// Patching HasNode related methods for our exclude: filter type
@ -384,9 +437,9 @@ class Scout : Plugin() {
val opt = field.get(this) as HasAnswerOption
if (filterType == FilterType.HAS)
builder.appendParam("has", opt.restParamValue);
builder.appendParam("has", opt.restParamValue)
else if (filterType == FilterTypeExtension.EXCLUDE)
builder.appendParam("has", "-" + opt.restParamValue);
builder.appendParam("has", "-" + opt.restParamValue)
}
// Patching the behaviour when the has suggestion is clicked
@ -416,8 +469,6 @@ class Scout : Plugin() {
)
getAnswerReplacementStart.isAccessible = true
logger.info(query.joinToString("|") { it.text })
val replacementIdx = getAnswerReplacementStart.invoke(this, query) as Int
val previousFilterText = query[replacementIdx]
val filterNode = if (previousFilterText.text == ssProvider.excludeFilterString)
@ -441,6 +492,7 @@ class Scout : Plugin() {
var minID = params["min_id"]
var maxID = params["max_id"]
val sortOrder = params["sort_order"]
val authorType = params["author_type"]
self.`$oldestMessageId`?.let {
if (sortOrder?.getOrNull(0) == "asc")
minID = listOf(it.toString())
@ -461,7 +513,8 @@ class Scout : Plugin() {
retryAttempts,
self.`$searchQuery`.includeNsfw,
listOf("timestamp"),
sortOrder
sortOrder,
authorType,
)
else
searchApi.searchChannelMessages(
@ -475,7 +528,8 @@ class Scout : Plugin() {
retryAttempts,
self.`$searchQuery`.includeNsfw,
listOf("timestamp"),
sortOrder
sortOrder,
authorType,
)
}
)
@ -495,6 +549,8 @@ class Scout : Plugin() {
DateNode.getDateRule(),
SortNode.getFilterRule(ssProvider.sortFilterString),
SortNode.getSortRule(ssProvider),
AuthorTypeNode.getFilterRule(ssProvider.authorTypeFilter),
AuthorTypeNode.getAuthorTypesRule(),
SimpleParserRule(Pattern.compile("^\\s*?${ssProvider.excludeFilterString}:", 64)) { _, _, obj ->
ParseSpec(FilterNode(FilterTypeExtension.EXCLUDE, ssProvider.excludeFilterString), obj)
}
@ -503,6 +559,7 @@ class Scout : Plugin() {
}
// This is probably the worst bit of this plugin
@SuppressLint("SetTextI18n")
private fun patchSearchUI(context: Context) {
// Run when a filter suggestion is clicked
// Most of the code is copied from its implementation
@ -515,7 +572,7 @@ class Scout : Plugin() {
) { param ->
val filter = param.args[0] as FilterType
if (filter !in FilterTypeExtension.values)
return@before; // Exit if not an extended filter type
return@before // Exit if not an extended filter type
val replaceAndPublish = StoreSearchInput::class.java.getDeclaredMethod(
"replaceAndPublish",
@ -549,7 +606,7 @@ class Scout : Plugin() {
getAnswerReplacementStart.invoke(this, list),
listOf(filterNode, DateNode(it)),
list
);
)
}
}
@ -558,14 +615,21 @@ class Scout : Plugin() {
lastIndex,
listOf(filterNode, SortNode(ssProvider.sortOldString)),
list
);
)
if (filter == FilterTypeExtension.EXCLUDE)
replaceAndPublish.invoke(this,
lastIndex,
listOf(filterNode),
list
);
)
if (filter == FilterTypeExtension.AUTHOR_TYPE)
replaceAndPublish.invoke(this,
lastIndex,
listOf(filterNode),
list
)
param.result = null
}
@ -584,6 +648,7 @@ class Scout : Plugin() {
FilterTypeExtension.AFTER -> false to scoutRes.getDrawableId("baseline_update_24")
FilterTypeExtension.SORT -> true to R.e.ic_sort_white_24dp
FilterTypeExtension.EXCLUDE -> false to scoutRes.getDrawableId("baseline_do_disturb_on_24")
FilterTypeExtension.AUTHOR_TYPE -> true to R.e.ic_members_24dp
else -> false to null
}
@ -605,6 +670,8 @@ class Scout : Plugin() {
param.result = ScoutResource.SORT_ANSWER
if (type == FilterTypeExtension.EXCLUDE)
param.result = ssProvider.getIdentifier("search_answer_has")
if (type == FilterTypeExtension.AUTHOR_TYPE)
param.result = ScoutResource.AUTHOR_TYPE_ANSWER
}
// Patch for retrieving filter name
@ -619,6 +686,7 @@ class Scout : Plugin() {
FilterTypeExtension.DURING -> ssProvider.getIdentifier("search_filter_during")
FilterTypeExtension.AFTER -> ssProvider.getIdentifier("search_filter_after")
FilterTypeExtension.SORT -> ScoutResource.SORT_FILTER
FilterTypeExtension.AUTHOR_TYPE -> ScoutResource.AUTHOR_TYPE_FILTER
else -> null
}
res?.let { param.result = it }
@ -641,6 +709,8 @@ class Scout : Plugin() {
ScoutResource.SORT_FILTER -> ssProvider.sortFilterString
ScoutResource.SORT_ANSWER -> ssProvider.sortOldString
ScoutResource.EXCLUDE_FILTER -> ssProvider.excludeFilterString
ScoutResource.AUTHOR_TYPE_FILTER -> ssProvider.authorTypeFilter
ScoutResource.AUTHOR_TYPE_ANSWER -> ssProvider.authorTypeAnswer
else -> null
}
override?.let {
@ -700,6 +770,56 @@ class Scout : Plugin() {
}
param.result = res.toList()
}
// Patch to add header for new categories
patcher.before<WidgetSearchSuggestionsAdapter.HeaderViewHolder>(
"onConfigure",
Int::class.javaPrimitiveType!!,
MGRecyclerDataPayload::class.java,
) { (param, _: Int, payload: SingleTypePayload<SearchSuggestion.Category>) ->
val category = payload.data
if (category == SuggestionCategoryExtension.AUTHOR_TYPE) {
binding.b.text = "Author Type"
param.result = null
}
}
// Patch to add entries depending on category
patcher.after<WidgetSearchSuggestions.Model>(
List::class.java,
List::class.java,
) { (_, _: List<QueryNode>, suggestions: List<SearchSuggestion>) ->
var lastCategory: SearchSuggestion.Category? = null
val newItems = mutableListOf<MGRecyclerDataPayload>()
suggestions.forEach {
if (it is AuthorTypeSuggestion) {
if (lastCategory != it.category) {
newItems.add(
SingleTypePayload(it.category, it.category.name, 0)
)
lastCategory = it.category
}
newItems.add(
SingleTypePayload(it, it.type.value, SuggestionCategoryExtension.AdapterType.AUTHOR_TYPE)
)
}
}
suggestionItems.removeAll { it in newItems }
suggestionItems.addAll(0, newItems)
}
// Patch to add new types of suggestion entries
patcher.before<WidgetSearchSuggestionsAdapter>(
"onCreateViewHolder",
ViewGroup::class.java,
Int::class.javaPrimitiveType!!,
) { (param, _: ViewGroup, id: Int) ->
when (id) {
SuggestionCategoryExtension.AdapterType.AUTHOR_TYPE -> {
param.result = AuthorTypeViewHolder(this, scoutRes)
}
}
}
}
// Adds support for searching in threads
@ -707,7 +827,7 @@ class Scout : Plugin() {
// Patch query parser for in: to support names with spaces, by wrapping them in quotes
// This enables searching for threads which can have spaces in their names
patcher.instead<QueryParser.Companion>("getInAnswerRule") {
val compile = Pattern.compile("^\\s*#(\".*?\"|[^ ]+)", 64);
val compile = Pattern.compile("^\\s*#(\".*?\"|[^ ]+)", 64)
`QueryParser$Companion$getInAnswerRule$1`(compile, compile)
}
@ -802,7 +922,7 @@ class Scout : Plugin() {
// Now it matches something like @<username>[#<discrim>] (bots still have discriminators)
// The @ is required unfortunately, to distinguish it from literally any other word
patcher.instead<QueryParser.Companion>("getUserRule") {
val regex = Pattern.compile("^\\s*@(?:([^@#:]+)#([0-9]{4})|([a-z0-9._]{2,32}))", 64);
val regex = Pattern.compile("^\\s*@(?:([^@#:]+)#([0-9]{4})|([a-z0-9._]{2,32}))", 64)
// Returns a new rule to support our optional second group (discriminator)
return@instead SimpleParserRule(regex) { matcher, _, obj ->

View file

@ -0,0 +1,12 @@
package moe.lava.awoocord.scout
import com.discord.utilities.search.suggestion.entries.SearchSuggestion
object SuggestionCategoryExtension {
lateinit var AUTHOR_TYPE: SearchSuggestion.Category
lateinit var values: Array<SearchSuggestion.Category>
object AdapterType {
const val AUTHOR_TYPE = 7
}
}

View file

@ -24,6 +24,7 @@ interface SearchAPIInterface {
@t("include_nsfw") includeNsfw: Boolean?,
@t("sort_by") sortBy: List<String>?, // "timestamp" is one, not sure about any other sort types
@t("sort_order") sortOrder: List<String>?, // "asc" or "desc"
@t("author_type") authorType: List<String>?,
): Observable<ModelSearchResponse?>
@f("guilds/{guildId}/messages/search")
@ -40,5 +41,6 @@ interface SearchAPIInterface {
@t("include_nsfw") includeNsfw: Boolean?,
@t("sort_by") sortBy: List<String>?,
@t("sort_order") sortOrder: List<String>?,
@t("author_type") authorType: List<String>?,
): Observable<ModelSearchResponse?>
}

View file

@ -0,0 +1,9 @@
package moe.lava.awoocord.scout.entries
import com.discord.utilities.search.suggestion.entries.SearchSuggestion
import moe.lava.awoocord.scout.SuggestionCategoryExtension
import moe.lava.awoocord.scout.parsing.AuthorType
data class AuthorTypeSuggestion(val type: AuthorType) : SearchSuggestion {
override fun getCategory() = SuggestionCategoryExtension.AUTHOR_TYPE
}

View file

@ -0,0 +1,77 @@
package moe.lava.awoocord.scout.entries
import android.widget.ImageView
import android.widget.TextView
import com.aliucord.Utils
import com.aliucord.utils.ViewUtils.findViewById
import com.discord.stores.StoreSearchInput
import com.discord.stores.StoreStream
import com.discord.utilities.mg_recycler.MGRecyclerDataPayload
import com.discord.utilities.mg_recycler.MGRecyclerViewHolder
import com.discord.utilities.mg_recycler.SingleTypePayload
import com.discord.utilities.search.query.node.filter.FilterNode
import com.discord.widgets.search.suggestions.`WidgetSearchSuggestions$configureUI$4`
import com.discord.widgets.search.suggestions.WidgetSearchSuggestionsAdapter
import com.lytefast.flexinput.R
import moe.lava.awoocord.scout.FilterTypeExtension
import moe.lava.awoocord.scout.parsing.AuthorType
import moe.lava.awoocord.scout.parsing.AuthorTypeNode
import moe.lava.awoocord.scout.ui.ScoutResource
private val replaceAndPublish = StoreSearchInput::class.java.getDeclaredMethod(
"replaceAndPublish",
Int::class.javaPrimitiveType!!,
List::class.java,
List::class.java
).apply { isAccessible = true }
private val getAnswerReplacementStart = StoreSearchInput::class.java.getDeclaredMethod(
"getAnswerReplacementStart",
List::class.java,
).apply { isAccessible = true }
class AuthorTypeViewHolder(
adapter: WidgetSearchSuggestionsAdapter,
// This should be fine (?)
private val scoutRes: ScoutResource,
) : MGRecyclerViewHolder<WidgetSearchSuggestionsAdapter, MGRecyclerDataPayload>(
Utils.getResId("widget_search_suggestions_item_has", "layout"),
adapter,
) {
private val imageView = itemView.findViewById<ImageView>("search_suggestions_item_has_icon")
private val textView = itemView.findViewById<TextView>("search_suggestions_item_has_text")
override fun onConfigure(i: Int, oPayload: MGRecyclerDataPayload) {
super.onConfigure(i, oPayload)
@Suppress("UNCHECKED_CAST")
val payload = oPayload as SingleTypePayload<AuthorTypeSuggestion>
val type = payload.data.type
textView.text = when (type) {
AuthorType.Bot -> "bot"
AuthorType.User -> "user"
AuthorType.Webhook -> "webhook"
}
when (type) {
AuthorType.Bot -> imageView.setImageDrawable(scoutRes.getDrawable("smart_toy_24px"))
AuthorType.User -> imageView.setImageResource(R.e.ic_members_24dp)
AuthorType.Webhook -> imageView.setImageDrawable(scoutRes.getDrawable("webhook_24px"))
}
itemView.setOnClickListener {
val hasHandler = adapter.onHasClicked as `WidgetSearchSuggestions$configureUI$4`
val query = hasHandler.`$model`.query
val storeInput = StoreStream.getSearch().storeSearchInput
replaceAndPublish.invoke(
storeInput,
getAnswerReplacementStart.invoke(storeInput, query) as Int,
listOf(
FilterNode(FilterTypeExtension.AUTHOR_TYPE, "authorType"),
AuthorTypeNode(type)
),
query,
)
}
}
}

View file

@ -0,0 +1,62 @@
package moe.lava.awoocord.scout.parsing
import android.content.Context
import com.discord.simpleast.core.parser.ParseSpec
import com.discord.simpleast.core.parser.Rule
import com.discord.utilities.search.network.SearchQuery
import com.discord.utilities.search.query.FilterType
import com.discord.utilities.search.query.node.QueryNode
import com.discord.utilities.search.query.node.answer.AnswerNode
import com.discord.utilities.search.query.node.filter.FilterNode
import com.discord.utilities.search.validation.SearchData
import moe.lava.awoocord.scout.FilterTypeExtension
import java.util.regex.Pattern
// TODO: not localised, maybe one day
enum class AuthorType(val value: String) {
User("user"),
Bot("bot"),
Webhook("webhook"),
;
companion object {
fun from(value: String) = when (value) {
"user" -> User
"bot" -> Bot
"webhook" -> Webhook
else -> throw IllegalArgumentException("Unknown author type $value")
}
}
}
class AuthorTypeNode(val type: AuthorType): AnswerNode() {
companion object {
fun getAuthorTypesRule(): Rule<Context, QueryNode, Any> {
val joined = AuthorType.values().joinToString("|") { it.value }
val regexStr = "^\\s*(${joined})"
val regex = Pattern.compile(regexStr, Pattern.UNICODE_CASE)
return SimpleParserRule(regex) { matcher, _, obj ->
ParseSpec(AuthorTypeNode(AuthorType.from(matcher.group())), obj)
}
}
fun getFilterRule(str: String): ParserRule {
val regex = Pattern.compile("^\\s*?(${str}):", 64);
return SimpleParserRule(regex) { _, _, obj ->
ParseSpec(FilterNode(FilterTypeExtension.AUTHOR_TYPE, str), obj)
}
}
}
override fun getValidFilters() = setOf(FilterTypeExtension.AUTHOR_TYPE)
override fun isValid(searchData: SearchData?) = true
override fun getText() = type.value
override fun updateQuery(
builder: SearchQuery.Builder,
searchData: SearchData?,
filterType: FilterType?
) {
builder.appendParam("author_type", type.value)
}
}

View file

@ -36,11 +36,10 @@ class SortNode(private val text: String): AnswerNode() {
override fun getText() = this.text
override fun updateQuery(
builder: SearchQuery.Builder?,
builder: SearchQuery.Builder,
searchData: SearchData?,
filterType: FilterType?
) {
checkNotNull(builder) { "queryBuilder" }
builder.appendParam("sort_order", "asc")
}
}

View file

@ -10,6 +10,8 @@ class ScoutResource(private val resources: Resources) {
val SORT_FILTER = View.generateViewId()
val SORT_ANSWER = View.generateViewId()
val EXCLUDE_FILTER = View.generateViewId()
val AUTHOR_TYPE_FILTER = View.generateViewId()
val AUTHOR_TYPE_ANSWER = View.generateViewId()
}
fun getId(name: String, type: String) =

View file

@ -21,6 +21,7 @@ class ScoutSearchStringProvider(private val context: Context) {
FilterTypeExtension.DURING -> duringFilterString
FilterTypeExtension.AFTER -> afterFilterString
FilterTypeExtension.SORT -> sortFilterString
FilterTypeExtension.AUTHOR_TYPE -> authorTypeFilter
else -> throw IllegalArgumentException("invalid extended filter type")
}
@ -51,4 +52,9 @@ class ScoutSearchStringProvider(private val context: Context) {
get() = "forward"
val excludeFilterString: String
get() = "exclude"
val authorTypeFilter: String
get() = "authorType"
val authorTypeAnswer: String
// TODO, could probably be localisable by joining each part together
get() = "user, bot or webhook"
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="#FFFFFF">
<path
android:fillColor="@android:color/white"
android:pathData="M160,600Q110,600 75,565Q40,530 40,480Q40,430 75,395Q110,360 160,360L160,280Q160,247 183.5,223.5Q207,200 240,200L360,200Q360,150 395,115Q430,80 480,80Q530,80 565,115Q600,150 600,200L720,200Q753,200 776.5,223.5Q800,247 800,280L800,360Q850,360 885,395Q920,430 920,480Q920,530 885,565Q850,600 800,600L800,760Q800,793 776.5,816.5Q753,840 720,840L240,840Q207,840 183.5,816.5Q160,793 160,760L160,600ZM402.5,502.5Q420,485 420,460Q420,435 402.5,417.5Q385,400 360,400Q335,400 317.5,417.5Q300,435 300,460Q300,485 317.5,502.5Q335,520 360,520Q385,520 402.5,502.5ZM642.5,502.5Q660,485 660,460Q660,435 642.5,417.5Q625,400 600,400Q575,400 557.5,417.5Q540,435 540,460Q540,485 557.5,502.5Q575,520 600,520Q625,520 642.5,502.5ZM320,680L640,680L640,600L320,600L320,680ZM240,760L720,760Q720,760 720,760Q720,760 720,760L720,280Q720,280 720,280Q720,280 720,280L240,280Q240,280 240,280Q240,280 240,280L240,760Q240,760 240,760Q240,760 240,760ZM480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520Z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="#FFFFFF">
<path
android:fillColor="@android:color/white"
android:pathData="M280,840Q197,840 138.5,781.5Q80,723 80,640Q80,567 125.5,512.5Q171,458 240,444L240,527Q205,539 182.5,570Q160,601 160,640Q160,690 195,725Q230,760 280,760Q330,760 365,725Q400,690 400,640L400,600L635,600Q643,591 654.5,585.5Q666,580 680,580Q705,580 722.5,597.5Q740,615 740,640Q740,665 722.5,682.5Q705,700 680,700Q666,700 654.5,694.5Q643,689 635,680L476,680Q462,749 407.5,794.5Q353,840 280,840ZM680,840Q624,840 578.5,812.5Q533,785 507,740L614,740Q628,750 645,755Q662,760 680,760Q730,760 765,725Q800,690 800,640Q800,590 765,555Q730,520 680,520Q660,520 643,525.5Q626,531 611,542L489,339Q468,335 454,319Q440,303 440,280Q440,255 457.5,237.5Q475,220 500,220Q525,220 542.5,237.5Q560,255 560,280Q560,285 560,288.5Q560,292 558,297L645,443Q653,441 662,440.5Q671,440 680,440Q763,440 821.5,498.5Q880,557 880,640Q880,723 821.5,781.5Q763,840 680,840ZM280,700Q255,700 237.5,682.5Q220,665 220,640Q220,618 234,602Q248,586 268,581L362,425Q333,398 316.5,360.5Q300,323 300,280Q300,197 358.5,138.5Q417,80 500,80Q583,80 641.5,138.5Q700,197 700,280L620,280Q620,230 585,195Q550,160 500,160Q450,160 415,195Q380,230 380,280Q380,323 406,355.5Q432,388 472,397L337,622Q339,627 339.5,631Q340,635 340,640Q340,665 322.5,682.5Q305,700 280,700Z"/>
</vector>