From e4ab9f936ded3f8e6a5358502cf876b2676b4725 Mon Sep 17 00:00:00 2001 From: Cilly Leang Date: Wed, 18 Feb 2026 03:14:40 +1100 Subject: [PATCH] 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 --- .../awoocord/scout/FilterTypeExtension.kt | 1 + .../kotlin/moe/lava/awoocord/scout/Scout.kt | 190 ++++++++++++++---- .../scout/SuggestionCategoryExtension.kt | 12 ++ .../awoocord/scout/api/SearchAPIInterface.kt | 2 + .../scout/entries/AuthorTypeSuggestion.kt | 9 + .../scout/entries/AuthorTypeViewHolder.kt | 77 +++++++ .../awoocord/scout/parsing/AuthorTypeNode.kt | 62 ++++++ .../lava/awoocord/scout/parsing/SortNode.kt | 3 +- .../lava/awoocord/scout/ui/ScoutResource.kt | 2 + .../scout/ui/ScoutSearchStringProvider.kt | 6 + .../src/main/res/drawable/smart_toy_24px.xml | 10 + .../src/main/res/drawable/webhook_24px.xml | 10 + 12 files changed, 347 insertions(+), 37 deletions(-) create mode 100644 plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/SuggestionCategoryExtension.kt create mode 100644 plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/entries/AuthorTypeSuggestion.kt create mode 100644 plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/entries/AuthorTypeViewHolder.kt create mode 100644 plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/AuthorTypeNode.kt create mode 100644 plugins/Scout/src/main/res/drawable/smart_toy_24px.xml create mode 100644 plugins/Scout/src/main/res/drawable/webhook_24px.xml diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/FilterTypeExtension.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/FilterTypeExtension.kt index 66ff114..58afef0 100644 --- a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/FilterTypeExtension.kt +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/FilterTypeExtension.kt @@ -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 lateinit var filters: Array lateinit var values: Array diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/Scout.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/Scout.kt index f4580af..c32ddda 100644 --- a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/Scout.kt +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/Scout.kt @@ -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() -@AliucordPlugin() +private val WidgetSearchSuggestionsAdapter.HeaderViewHolder.binding + by accessField() + +@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? = 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? = 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? = 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 + 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() - - val res = mutableListOf() - 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() + } } // 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( + "onConfigure", + Int::class.javaPrimitiveType!!, + MGRecyclerDataPayload::class.java, + ) { (param, _: Int, payload: SingleTypePayload) -> + 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( + List::class.java, + List::class.java, + ) { (_, _: List, suggestions: List) -> + var lastCategory: SearchSuggestion.Category? = null + val newItems = mutableListOf() + 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( + "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("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 @[#] (bots still have discriminators) // The @ is required unfortunately, to distinguish it from literally any other word patcher.instead("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 -> diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/SuggestionCategoryExtension.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/SuggestionCategoryExtension.kt new file mode 100644 index 0000000..2fcc40b --- /dev/null +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/SuggestionCategoryExtension.kt @@ -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 + + object AdapterType { + const val AUTHOR_TYPE = 7 + } +} diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/api/SearchAPIInterface.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/api/SearchAPIInterface.kt index 6bbe273..c3b45e4 100644 --- a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/api/SearchAPIInterface.kt +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/api/SearchAPIInterface.kt @@ -24,6 +24,7 @@ interface SearchAPIInterface { @t("include_nsfw") includeNsfw: Boolean?, @t("sort_by") sortBy: List?, // "timestamp" is one, not sure about any other sort types @t("sort_order") sortOrder: List?, // "asc" or "desc" + @t("author_type") authorType: List?, ): Observable @f("guilds/{guildId}/messages/search") @@ -40,5 +41,6 @@ interface SearchAPIInterface { @t("include_nsfw") includeNsfw: Boolean?, @t("sort_by") sortBy: List?, @t("sort_order") sortOrder: List?, + @t("author_type") authorType: List?, ): Observable } diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/entries/AuthorTypeSuggestion.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/entries/AuthorTypeSuggestion.kt new file mode 100644 index 0000000..b989e03 --- /dev/null +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/entries/AuthorTypeSuggestion.kt @@ -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 +} diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/entries/AuthorTypeViewHolder.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/entries/AuthorTypeViewHolder.kt new file mode 100644 index 0000000..afb1161 --- /dev/null +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/entries/AuthorTypeViewHolder.kt @@ -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( + Utils.getResId("widget_search_suggestions_item_has", "layout"), + adapter, +) { + private val imageView = itemView.findViewById("search_suggestions_item_has_icon") + private val textView = itemView.findViewById("search_suggestions_item_has_text") + + override fun onConfigure(i: Int, oPayload: MGRecyclerDataPayload) { + super.onConfigure(i, oPayload) + + @Suppress("UNCHECKED_CAST") + val payload = oPayload as SingleTypePayload + 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, + ) + } + } +} diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/AuthorTypeNode.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/AuthorTypeNode.kt new file mode 100644 index 0000000..e3ab895 --- /dev/null +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/AuthorTypeNode.kt @@ -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 { + 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) + } +} diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/SortNode.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/SortNode.kt index e74f2a9..b72b28a 100644 --- a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/SortNode.kt +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/SortNode.kt @@ -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") } } diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutResource.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutResource.kt index 38c9e02..59b9ed7 100644 --- a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutResource.kt +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutResource.kt @@ -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) = diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutSearchStringProvider.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutSearchStringProvider.kt index fb773b2..d4accb2 100644 --- a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutSearchStringProvider.kt +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutSearchStringProvider.kt @@ -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" } diff --git a/plugins/Scout/src/main/res/drawable/smart_toy_24px.xml b/plugins/Scout/src/main/res/drawable/smart_toy_24px.xml new file mode 100644 index 0000000..30dd4b2 --- /dev/null +++ b/plugins/Scout/src/main/res/drawable/smart_toy_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/plugins/Scout/src/main/res/drawable/webhook_24px.xml b/plugins/Scout/src/main/res/drawable/webhook_24px.xml new file mode 100644 index 0000000..74a3dc7 --- /dev/null +++ b/plugins/Scout/src/main/res/drawable/webhook_24px.xml @@ -0,0 +1,10 @@ + + +